From c708698a5d0701d3c2ddd150d4adc10188c1ded8 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sat, 24 Apr 2021 12:14:30 -0700 Subject: [PATCH 01/47] Shorten nonascii source file and script names --- packages/adl/compiler/character-codes.ts | 2 +- packages/adl/compiler/{non-ascii-maps.ts => nonascii.ts} | 0 packages/adl/package.json | 2 +- .../adl/scripts/{regen-nonascii-maps.js => regen-nonascii.js} | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/adl/compiler/{non-ascii-maps.ts => nonascii.ts} (100%) rename packages/adl/scripts/{regen-nonascii-maps.js => regen-nonascii.js} (98%) diff --git a/packages/adl/compiler/character-codes.ts b/packages/adl/compiler/character-codes.ts index a6efa66fc..feac1d429 100644 --- a/packages/adl/compiler/character-codes.ts +++ b/packages/adl/compiler/character-codes.ts @@ -1,4 +1,4 @@ -import { nonAsciiIdentifierContinueMap, nonAsciiIdentifierStartMap } from "./non-ascii-maps.js"; +import { nonAsciiIdentifierContinueMap, nonAsciiIdentifierStartMap } from "./nonascii.js"; export const enum CharacterCodes { nullCharacter = 0, diff --git a/packages/adl/compiler/non-ascii-maps.ts b/packages/adl/compiler/nonascii.ts similarity index 100% rename from packages/adl/compiler/non-ascii-maps.ts rename to packages/adl/compiler/nonascii.ts diff --git a/packages/adl/package.json b/packages/adl/package.json index 3c32280d5..f03fd7a97 100644 --- a/packages/adl/package.json +++ b/packages/adl/package.json @@ -39,7 +39,7 @@ "dogfood": "node scripts/dogfood.js", "test": "mocha --timeout 5000 --require source-map-support/register --ignore 'dist/test/manual/**/*.js' 'dist/test/**/*.js'", "regen-samples": "node scripts/regen-samples.js", - "regen-nonascii-maps": "node scripts/regen-nonascii-maps.js", + "regen-nonascii": "node scripts/regen-nonascii.js", "fuzz": "node dist/test/manual/fuzz.js" }, "dependencies": { diff --git a/packages/adl/scripts/regen-nonascii-maps.js b/packages/adl/scripts/regen-nonascii.js similarity index 98% rename from packages/adl/scripts/regen-nonascii-maps.js rename to packages/adl/scripts/regen-nonascii.js index 94a70e630..a85d5b7f3 100644 --- a/packages/adl/scripts/regen-nonascii-maps.js +++ b/packages/adl/scripts/regen-nonascii.js @@ -77,5 +77,5 @@ ${formatPairs(continueMap)} ]; `; -const file = resolve(fileURLToPath(import.meta.url), "../../compiler/non-ascii-maps.ts"); +const file = resolve(fileURLToPath(import.meta.url), "../../compiler/nonascii.ts"); writeFileSync(file, src); From 586018e47b6d0cdce182855408fe9c3df62c279b Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sat, 24 Apr 2021 12:27:26 -0700 Subject: [PATCH 02/47] Shorten charcode filename --- packages/adl/compiler/{character-codes.ts => charcode.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/adl/compiler/{character-codes.ts => charcode.ts} (100%) diff --git a/packages/adl/compiler/character-codes.ts b/packages/adl/compiler/charcode.ts similarity index 100% rename from packages/adl/compiler/character-codes.ts rename to packages/adl/compiler/charcode.ts From 8ba26ec85b94e992744c2e44bb1e9aa6da32dc77 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sat, 24 Apr 2021 12:55:38 -0700 Subject: [PATCH 03/47] Rename CharacterCodes to CharCode and use Pascal case for members Also re-group members with comments --- packages/adl/compiler/charcode.ts | 196 ++++++++++++++------------- packages/adl/compiler/diagnostics.ts | 12 +- packages/adl/compiler/scanner.ts | 172 +++++++++++------------ 3 files changed, 188 insertions(+), 192 deletions(-) diff --git a/packages/adl/compiler/charcode.ts b/packages/adl/compiler/charcode.ts index feac1d429..62adfe791 100644 --- a/packages/adl/compiler/charcode.ts +++ b/packages/adl/compiler/charcode.ts @@ -1,38 +1,45 @@ import { nonAsciiIdentifierContinueMap, nonAsciiIdentifierStartMap } from "./nonascii.js"; -export const enum CharacterCodes { - nullCharacter = 0, - maxAsciiCharacter = 0x7f, +export const enum CharCode { + Null = 0x00, + MaxAscii = 0x7f, - lineFeed = 0x0a, - carriageReturn = 0x0d, - lineSeparator = 0x2028, - paragraphSeparator = 0x2029, - nextLine = 0x0085, + // ASCII line breaks + LineFeed = 0x0a, + CarriageReturn = 0x0d, - // Unicode 3.0 space characters - space = 0x0020, - nonBreakingSpace = 0x00a0, - enQuad = 0x2000, - emQuad = 0x2001, - enSpace = 0x2002, - emSpace = 0x2003, - threePerEmSpace = 0x2004, - fourPerEmSpace = 0x2005, - sixPerEmSpace = 0x2006, - figureSpace = 0x2007, - punctuationSpace = 0x2008, - thinSpace = 0x2009, - hairSpace = 0x200a, - zeroWidthSpace = 0x200b, - narrowNoBreakSpace = 0x202f, - ideographicSpace = 0x3000, - mathematicalSpace = 0x205f, - ogham = 0x1680, + // Non-ASCII line breaks + LineSeparator = 0x2028, + ParagraphSeparator = 0x2029, - _ = 0x5f, - $ = 0x24, + // ASCII whitespace + Space = 0x20, + FormFeed = 0x0c, + Tab = 0x09, + VerticalTab = 0x0b, + // Non-ASCII whitespace + ByteOrderMark = 0xfeff, // currently allowed anywhere + NextLine = 0x0085, // not considered a line break, mirroring ECMA-262 + NonBreakingSpace = 0x00a0, + EnQuad = 0x2000, + EmQuad = 0x2001, + EnSpace = 0x2002, + EmSpace = 0x2003, + ThreePerEmSpace = 0x2004, + FourPerEmSpace = 0x2005, + SixPerEmSpace = 0x2006, + FigureSpace = 0x2007, + PunctuationSpace = 0x2008, + ThinSpace = 0x2009, + HairSpace = 0x200a, + ZeroWidthSpace = 0x200b, + NarrowNoBreakSpace = 0x202f, + IdeographicSpace = 0x3000, + MathematicalSpace = 0x205f, + Ogham = 0x1680, + + // ASCII Digits _0 = 0x30, _1 = 0x31, _2 = 0x32, @@ -44,6 +51,7 @@ export const enum CharacterCodes { _8 = 0x38, _9 = 0x39, + // ASCII lowercase letters a = 0x61, b = 0x62, c = 0x63, @@ -71,6 +79,7 @@ export const enum CharacterCodes { y = 0x79, z = 0x7a, + // ASCII uppercase letters A = 0x41, B = 0x42, C = 0x43, @@ -98,42 +107,41 @@ export const enum CharacterCodes { Y = 0x59, Z = 0x5a, - ampersand = 0x26, - asterisk = 0x2a, - at = 0x40, - backslash = 0x5c, - backtick = 0x60, - bar = 0x7c, - caret = 0x5e, - closeBrace = 0x7d, - closeBracket = 0x5d, - closeParen = 0x29, - colon = 0x3a, - comma = 0x2c, - dot = 0x2e, - doubleQuote = 0x22, - equals = 0x3d, - exclamation = 0x21, - greaterThan = 0x3e, - hash = 0x23, - lessThan = 0x3c, - minus = 0x2d, - openBrace = 0x7b, - openBracket = 0x5b, - openParen = 0x28, - percent = 0x25, - plus = 0x2b, - question = 0x3f, - semicolon = 0x3b, - singleQuote = 0x27, - slash = 0x2f, - tilde = 0x7e, + // Non-letter, non-digit ASCII characters that are valid in identifiers + _ = 0x5f, + $ = 0x24, - backspace = 0x08, - formFeed = 0x0c, - byteOrderMark = 0xfeff, - tab = 0x09, - verticalTab = 0x0b, + // ASCII punctuation + Ampersand = 0x26, + Asterisk = 0x2a, + At = 0x40, + Backslash = 0x5c, + Backtick = 0x60, + Bar = 0x7c, + Caret = 0x5e, + CloseBrace = 0x7d, + CloseBracket = 0x5d, + CloseParen = 0x29, + Colon = 0x3a, + Comma = 0x2c, + Dot = 0x2e, + DoubleQuote = 0x22, + Equals = 0x3d, + Exclamation = 0x21, + GreaterThan = 0x3e, + Hash = 0x23, + LessThan = 0x3c, + Minus = 0x2d, + OpenBrace = 0x7b, + OpenBracket = 0x5b, + OpenParen = 0x28, + Percent = 0x25, + Plus = 0x2b, + Question = 0x3f, + Semicolon = 0x3b, + SingleQuote = 0x27, + Slash = 0x2f, + Tilde = 0x7e, } /** Does not include line breaks. For that, see isWhiteSpaceLike. */ @@ -141,18 +149,18 @@ export function isWhiteSpaceSingleLine(ch: number): boolean { // Note: nextLine is in the Zs space, and should be considered to be a whitespace. // It is explicitly not a line-break as it isn't in the exact set specified by EcmaScript. return ( - ch === CharacterCodes.space || - ch === CharacterCodes.tab || - ch === CharacterCodes.verticalTab || - ch === CharacterCodes.formFeed || - ch === CharacterCodes.nonBreakingSpace || - ch === CharacterCodes.nextLine || - ch === CharacterCodes.ogham || - (ch >= CharacterCodes.enQuad && ch <= CharacterCodes.zeroWidthSpace) || - ch === CharacterCodes.narrowNoBreakSpace || - ch === CharacterCodes.mathematicalSpace || - ch === CharacterCodes.ideographicSpace || - ch === CharacterCodes.byteOrderMark + ch === CharCode.Space || + ch === CharCode.Tab || + ch === CharCode.VerticalTab || + ch === CharCode.FormFeed || + ch === CharCode.NonBreakingSpace || + ch === CharCode.NextLine || + ch === CharCode.Ogham || + (ch >= CharCode.EnQuad && ch <= CharCode.ZeroWidthSpace) || + ch === CharCode.NarrowNoBreakSpace || + ch === CharCode.MathematicalSpace || + ch === CharCode.IdeographicSpace || + ch === CharCode.ByteOrderMark ); } @@ -160,52 +168,50 @@ export function isLineBreak(ch: number): boolean { // Other new line or line // breaking characters are treated as white space but not as line terminators. return ( - ch === CharacterCodes.lineFeed || - ch === CharacterCodes.carriageReturn || - ch === CharacterCodes.lineSeparator || - ch === CharacterCodes.paragraphSeparator + ch === CharCode.LineFeed || + ch === CharCode.CarriageReturn || + ch === CharCode.LineSeparator || + ch === CharCode.ParagraphSeparator ); } export function isDigit(ch: number): boolean { - return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; + return ch >= CharCode._0 && ch <= CharCode._9; } export function isHexDigit(ch: number): boolean { return ( - isDigit(ch) || - (ch >= CharacterCodes.A && ch <= CharacterCodes.F) || - (ch >= CharacterCodes.a && ch <= CharacterCodes.f) + isDigit(ch) || (ch >= CharCode.A && ch <= CharCode.F) || (ch >= CharCode.a && ch <= CharCode.f) ); } export function isBinaryDigit(ch: number): boolean { - return ch === CharacterCodes._0 || ch === CharacterCodes._1; + return ch === CharCode._0 || ch === CharCode._1; } export function isAsciiIdentifierStart(ch: number): boolean { return ( - (ch >= CharacterCodes.A && ch <= CharacterCodes.Z) || - (ch >= CharacterCodes.a && ch <= CharacterCodes.z) || - ch === CharacterCodes.$ || - ch === CharacterCodes._ + (ch >= CharCode.A && ch <= CharCode.Z) || + (ch >= CharCode.a && ch <= CharCode.z) || + ch === CharCode.$ || + ch === CharCode._ ); } export function isAsciiIdentifierContinue(ch: number): boolean { return ( - (ch >= CharacterCodes.A && ch <= CharacterCodes.Z) || - (ch >= CharacterCodes.a && ch <= CharacterCodes.z) || - (ch >= CharacterCodes._0 && ch <= CharacterCodes._9) || - ch === CharacterCodes.$ || - ch === CharacterCodes._ + (ch >= CharCode.A && ch <= CharCode.Z) || + (ch >= CharCode.a && ch <= CharCode.z) || + (ch >= CharCode._0 && ch <= CharCode._9) || + ch === CharCode.$ || + ch === CharCode._ ); } export function isIdentifierContinue(codePoint: number) { return ( isAsciiIdentifierStart(codePoint) || - (codePoint > CharacterCodes.maxAsciiCharacter && isNonAsciiIdentifierContinue(codePoint)) + (codePoint > CharCode.MaxAscii && isNonAsciiIdentifierContinue(codePoint)) ); } diff --git a/packages/adl/compiler/diagnostics.ts b/packages/adl/compiler/diagnostics.ts index 7eee855cd..128aac1ff 100644 --- a/packages/adl/compiler/diagnostics.ts +++ b/packages/adl/compiler/diagnostics.ts @@ -1,5 +1,5 @@ import { AssertionError } from "assert"; -import { CharacterCodes } from "./character-codes.js"; +import { CharCode } from "./charcode.js"; import { Message } from "./messages.js"; import { Diagnostic, Node, SourceFile, SourceLocation, Sym, SyntaxKind, Type } from "./types.js"; @@ -146,14 +146,14 @@ export function createSourceFile(text: string, path: string): SourceFile { const ch = text.charCodeAt(pos); pos++; switch (ch) { - case CharacterCodes.carriageReturn: - if (text.charCodeAt(pos) === CharacterCodes.lineFeed) { + case CharCode.CarriageReturn: + if (text.charCodeAt(pos) === CharCode.LineFeed) { pos++; } // fallthrough - case CharacterCodes.lineFeed: - case CharacterCodes.lineSeparator: - case CharacterCodes.paragraphSeparator: + case CharCode.LineFeed: + case CharCode.LineSeparator: + case CharCode.ParagraphSeparator: starts.push(start); start = pos; break; diff --git a/packages/adl/compiler/scanner.ts b/packages/adl/compiler/scanner.ts index 53b43b42d..a0f5790dd 100644 --- a/packages/adl/compiler/scanner.ts +++ b/packages/adl/compiler/scanner.ts @@ -1,5 +1,5 @@ import { - CharacterCodes, + CharCode, isAsciiIdentifierContinue, isAsciiIdentifierStart, isBinaryDigit, @@ -10,7 +10,7 @@ import { isNonAsciiIdentifierContinue, isNonAsciiIdentifierStart, isWhiteSpaceSingleLine, -} from "./character-codes.js"; +} from "./charcode.js"; import { createSourceFile, Message, throwOnError } from "./diagnostics.js"; import { SourceFile } from "./types.js"; @@ -258,130 +258,130 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro if (!eof()) { const ch = input.charCodeAt(position); switch (ch) { - case CharacterCodes.carriageReturn: - if (lookAhead(1) === CharacterCodes.lineFeed) { + case CharCode.CarriageReturn: + if (lookAhead(1) === CharCode.LineFeed) { position++; } // fallthrough - case CharacterCodes.lineFeed: - case CharacterCodes.lineSeparator: - case CharacterCodes.paragraphSeparator: + case CharCode.LineFeed: + case CharCode.LineSeparator: + case CharCode.ParagraphSeparator: return next(Token.NewLine); - case CharacterCodes.tab: - case CharacterCodes.verticalTab: - case CharacterCodes.formFeed: - case CharacterCodes.space: - case CharacterCodes.nonBreakingSpace: - case CharacterCodes.ogham: - case CharacterCodes.enQuad: - case CharacterCodes.emQuad: - case CharacterCodes.enSpace: - case CharacterCodes.emSpace: - case CharacterCodes.threePerEmSpace: - case CharacterCodes.fourPerEmSpace: - case CharacterCodes.sixPerEmSpace: - case CharacterCodes.figureSpace: - case CharacterCodes.punctuationSpace: - case CharacterCodes.thinSpace: - case CharacterCodes.hairSpace: - case CharacterCodes.zeroWidthSpace: - case CharacterCodes.narrowNoBreakSpace: - case CharacterCodes.mathematicalSpace: - case CharacterCodes.ideographicSpace: - case CharacterCodes.byteOrderMark: + case CharCode.Tab: + case CharCode.VerticalTab: + case CharCode.FormFeed: + case CharCode.Space: + case CharCode.NonBreakingSpace: + case CharCode.Ogham: + case CharCode.EnQuad: + case CharCode.EmQuad: + case CharCode.EnSpace: + case CharCode.EmSpace: + case CharCode.ThreePerEmSpace: + case CharCode.FourPerEmSpace: + case CharCode.SixPerEmSpace: + case CharCode.FigureSpace: + case CharCode.PunctuationSpace: + case CharCode.ThinSpace: + case CharCode.HairSpace: + case CharCode.ZeroWidthSpace: + case CharCode.NarrowNoBreakSpace: + case CharCode.MathematicalSpace: + case CharCode.IdeographicSpace: + case CharCode.ByteOrderMark: return scanWhitespace(); - case CharacterCodes.openParen: + case CharCode.OpenParen: return next(Token.OpenParen); - case CharacterCodes.closeParen: + case CharCode.CloseParen: return next(Token.CloseParen); - case CharacterCodes.comma: + case CharCode.Comma: return next(Token.Comma); - case CharacterCodes.colon: + case CharCode.Colon: return next(Token.Colon); - case CharacterCodes.semicolon: + case CharCode.Semicolon: return next(Token.Semicolon); - case CharacterCodes.openBracket: + case CharCode.OpenBracket: return next(Token.OpenBracket); - case CharacterCodes.closeBracket: + case CharCode.CloseBracket: return next(Token.CloseBracket); - case CharacterCodes.openBrace: + case CharCode.OpenBrace: return next(Token.OpenBrace); - case CharacterCodes.closeBrace: + case CharCode.CloseBrace: return next(Token.CloseBrace); - case CharacterCodes.at: + case CharCode.At: return next(Token.At); - case CharacterCodes.question: + case CharCode.Question: return next(Token.Question); - case CharacterCodes.ampersand: + case CharCode.Ampersand: return next(Token.Ampersand); - case CharacterCodes.dot: - return lookAhead(1) === CharacterCodes.dot && lookAhead(2) === CharacterCodes.dot + case CharCode.Dot: + return lookAhead(1) === CharCode.Dot && lookAhead(2) === CharCode.Dot ? next(Token.Elipsis, 3) : next(Token.Dot); - case CharacterCodes.slash: + case CharCode.Slash: switch (lookAhead(1)) { - case CharacterCodes.slash: + case CharCode.Slash: return scanSingleLineComment(); - case CharacterCodes.asterisk: + case CharCode.Asterisk: return scanMultiLineComment(); } return scanInvalidCharacter(); - case CharacterCodes._0: + case CharCode._0: switch (lookAhead(1)) { - case CharacterCodes.x: + case CharCode.x: return scanHexNumber(); - case CharacterCodes.b: + case CharCode.b: return scanBinaryNumber(); } // fallthrough - case CharacterCodes._1: - case CharacterCodes._2: - case CharacterCodes._3: - case CharacterCodes._4: - case CharacterCodes._5: - case CharacterCodes._6: - case CharacterCodes._7: - case CharacterCodes._8: - case CharacterCodes._9: + case CharCode._1: + case CharCode._2: + case CharCode._3: + case CharCode._4: + case CharCode._5: + case CharCode._6: + case CharCode._7: + case CharCode._8: + case CharCode._9: return scanNumber(); - case CharacterCodes.lessThan: + case CharCode.LessThan: return isConflictMarker() ? next(Token.ConflictMarker, mergeConflictMarkerLength) : next(Token.LessThan); - case CharacterCodes.greaterThan: + case CharCode.GreaterThan: return isConflictMarker() ? next(Token.ConflictMarker, mergeConflictMarkerLength) : next(Token.GreaterThan); - case CharacterCodes.equals: + case CharCode.Equals: return isConflictMarker() ? next(Token.ConflictMarker, mergeConflictMarkerLength) : next(Token.Equals); - case CharacterCodes.bar: + case CharCode.Bar: return isConflictMarker() ? next(Token.ConflictMarker, mergeConflictMarkerLength) : next(Token.Bar); - case CharacterCodes.doubleQuote: + case CharCode.DoubleQuote: return scanString(); default: @@ -409,10 +409,7 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro return false; } } - return ( - ch === CharacterCodes.equals || - lookAhead(mergeConflictMarkerLength) === CharacterCodes.space - ); + return ch === CharCode.Equals || lookAhead(mergeConflictMarkerLength) === CharCode.Space; } } @@ -442,16 +439,16 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro let ch = input.charCodeAt(position); - if (ch === CharacterCodes.dot) { + if (ch === CharCode.Dot) { position++; scanDigits(); } ch = input.charCodeAt(position); - if (ch === CharacterCodes.e) { + if (ch === CharCode.e) { position++; ch = input.charCodeAt(position); - if (ch === CharacterCodes.plus || ch == CharacterCodes.minus) { + if (ch === CharCode.Plus || ch == CharCode.Minus) { position++; ch = input.charCodeAt(position); } @@ -520,11 +517,7 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro } function scanMultiLineComment() { - scanUntil( - (ch) => ch === CharacterCodes.asterisk && lookAhead(1) === CharacterCodes.slash, - "*/", - 2 - ); + scanUntil((ch) => ch === CharCode.Asterisk && lookAhead(1) === CharCode.Slash, "*/", 2); return (token = Token.MultiLineComment); } @@ -534,7 +527,7 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro let isEscaping = false; const tripleQuoted = - lookAhead(1) === CharacterCodes.doubleQuote && lookAhead(2) === CharacterCodes.doubleQuote; + lookAhead(1) === CharCode.DoubleQuote && lookAhead(2) === CharCode.DoubleQuote; if (tripleQuoted) { tokenFlags |= TokenFlags.TripleQuoted; @@ -551,23 +544,20 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro } switch (ch) { - case CharacterCodes.carriageReturn: - if (lookAhead(1) === CharacterCodes.lineFeed) { + case CharCode.CarriageReturn: + if (lookAhead(1) === CharCode.LineFeed) { tokenFlags |= TokenFlags.HasCrlf; } return false; - case CharacterCodes.backslash: + case CharCode.Backslash: isEscaping = true; tokenFlags |= TokenFlags.Escaped; return false; - case CharacterCodes.doubleQuote: + case CharCode.DoubleQuote: if (tripleQuoted) { - return ( - lookAhead(1) === CharacterCodes.doubleQuote && - lookAhead(2) === CharacterCodes.doubleQuote - ); + return lookAhead(1) === CharCode.DoubleQuote && lookAhead(2) === CharCode.DoubleQuote; } return true; @@ -699,7 +689,7 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro while (pos < end) { let ch = text.charCodeAt(pos); - if (ch != CharacterCodes.backslash) { + if (ch != CharCode.Backslash) { pos++; continue; } @@ -709,19 +699,19 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro ch = text.charCodeAt(pos); switch (ch) { - case CharacterCodes.r: + case CharCode.r: result += "\r"; break; - case CharacterCodes.n: + case CharCode.n: result += "\n"; break; - case CharacterCodes.t: + case CharCode.t: result += "\t"; break; - case CharacterCodes.doubleQuote: + case CharCode.DoubleQuote: result += '"'; break; - case CharacterCodes.backslash: + case CharCode.Backslash: result += "\\"; break; default: @@ -753,7 +743,7 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro ch = input.charCodeAt(position); } while (isAsciiIdentifierContinue(ch)); - if (!eof() && ch > CharacterCodes.maxAsciiCharacter) { + if (!eof() && ch > CharCode.MaxAscii) { const codePoint = input.codePointAt(position)!; if (isNonAsciiIdentifierContinue(codePoint)) { return scanNonAsciiIdentifierContinue(codePoint); From a60e5c209653e63ac87160e03ae87393e0e8718e Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sat, 24 Apr 2021 16:48:33 -0700 Subject: [PATCH 04/47] Pass along warnings and actually write out spec in watch build --- packages/adl-language/scripts/watch-spec.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/adl-language/scripts/watch-spec.js b/packages/adl-language/scripts/watch-spec.js index 6c0cb78d9..9a1e7279b 100644 --- a/packages/adl-language/scripts/watch-spec.js +++ b/packages/adl-language/scripts/watch-spec.js @@ -1,6 +1,6 @@ import watch from "watch"; import ecmarkup from "ecmarkup"; -import { readFile } from "fs/promises"; +import { readFile, writeFile } from "fs/promises"; import { runWatch } from "../../../eng/scripts/helpers.js"; import { resolve } from "path"; @@ -10,11 +10,25 @@ async function build() { const fetch = (path) => readFile(path, "utf-8"); try { - await ecmarkup.build(infile, fetch, { outfile }); + const spec = await ecmarkup.build(infile, fetch, { + outfile, + warn, + }); + for (const [file, contents] of spec.generatedFiles) { + await writeFile(file, contents); + } } catch (err) { console.log(`${infile}(1,1): error EMU0001: Error generating spec: ${err.message}`); throw err; } + + function warn(warning) { + const file = warning.file ?? infile; + const line = warning.line ?? 1; + const col = warning.column ?? 1; + const id = "EMU0002" + (warning.ruleId ? `: ${warning.ruleId}` : ""); + console.log(`${file}(${line},${col}): warning ${id}: ${warning.message}`); + } } runWatch(watch, "src", build); From df4b13e4c74aa8276504485426d65048301d3807 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sat, 24 Apr 2021 16:52:17 -0700 Subject: [PATCH 05/47] Use `===` --- packages/adl/compiler/parser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/adl/compiler/parser.ts b/packages/adl/compiler/parser.ts index c55356223..bd0209974 100644 --- a/packages/adl/compiler/parser.ts +++ b/packages/adl/compiler/parser.ts @@ -757,7 +757,7 @@ export function parse(code: string | SourceFile) { function parseBooleanLiteral(): BooleanLiteralNode { const pos = tokenPos(); const token = parseExpectedOneOf(Token.TrueKeyword, Token.FalseKeyword); - const value = token == Token.TrueKeyword; + const value = token === Token.TrueKeyword; return finishNode( { kind: SyntaxKind.BooleanLiteral, @@ -931,7 +931,7 @@ export function parse(code: string | SourceFile) { * open token is present. Otherwise, return an empty list. */ function parseOptionalList(kind: SurroundedListKind, parseItem: ParseListItem): T[] { - return token() == kind.open ? parseList(kind, parseItem) : []; + return token() === kind.open ? parseList(kind, parseItem) : []; } function parseOptionalDelimiter(kind: ListKind) { From 63858dae30d7e132e0a98ce1673f9caf97405d0f Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sat, 24 Apr 2021 16:52:44 -0700 Subject: [PATCH 06/47] Run ecmarkup with --strict --- packages/adl-language/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adl-language/package.json b/packages/adl-language/package.json index 9c7e88fd7..2afe0eafd 100644 --- a/packages/adl-language/package.json +++ b/packages/adl-language/package.json @@ -16,7 +16,7 @@ }, "type": "module", "scripts": { - "build": "ecmarkup src/spec.emu.html ../../docs/spec.html", + "build": "ecmarkup --strict src/spec.emu.html ../../docs/spec.html", "watch": "node scripts/watch-spec.js" }, "dependencies": {}, From 9063e1b63fcafe1dbe52c6a6167aee82540a55e8 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sat, 24 Apr 2021 18:14:15 -0700 Subject: [PATCH 07/47] Include error code in messages sent from language server --- packages/adl/server/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adl/server/server.ts b/packages/adl/server/server.ts index 3d56f8481..db8baf16d 100644 --- a/packages/adl/server/server.ts +++ b/packages/adl/server/server.ts @@ -52,7 +52,7 @@ function checkChange(change: TextDocumentChangeEvent) { const end = document.positionAt(each.end); const range = Range.create(start, end); const severity = convertSeverity(each.severity); - const diagnostic = Diagnostic.create(range, each.message, severity, "ADL"); + const diagnostic = Diagnostic.create(range, each.message, severity, each.code, "ADL"); diagnostics.push(diagnostic); } From 5fb28d63781de99a9ce3f1940f09f4bf422aaa9d Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sat, 24 Apr 2021 18:17:28 -0700 Subject: [PATCH 08/47] Fix scanning edge cases and optimize slightly * Bug fixes * tokenValue() was wrong if an identifier ever came right after a string literal. * There was an incorrect and confusing end-of-file error if file ends with numeric literal. Generally hardened and cleaned up EOF checking throughout. * Digits weren't allowed in identifier with non-ascii characters. * Perf * Add more ASCII fast paths * Inline scanUntil everywhere, and simplify each use to what it actually needed * Avoid eager substring allocation and map lookup for keywords in more cases --- packages/adl/compiler/charcode.ts | 45 ++-- packages/adl/compiler/diagnostics.ts | 110 ++++---- packages/adl/compiler/messages.ts | 18 +- packages/adl/compiler/parser.ts | 8 +- packages/adl/compiler/scanner.ts | 364 +++++++++++++++------------ packages/adl/test/test-parser.ts | 32 ++- packages/adl/test/test-scanner.ts | 142 +++++++---- 7 files changed, 425 insertions(+), 294 deletions(-) diff --git a/packages/adl/compiler/charcode.ts b/packages/adl/compiler/charcode.ts index 62adfe791..7545b3f32 100644 --- a/packages/adl/compiler/charcode.ts +++ b/packages/adl/compiler/charcode.ts @@ -12,13 +12,13 @@ export const enum CharCode { LineSeparator = 0x2028, ParagraphSeparator = 0x2029, - // ASCII whitespace + // ASCII whitespace excluding line breaks Space = 0x20, FormFeed = 0x0c, Tab = 0x09, VerticalTab = 0x0b, - // Non-ASCII whitespace + // Non-ASCII whitespace excluding line breaks ByteOrderMark = 0xfeff, // currently allowed anywhere NextLine = 0x0085, // not considered a line break, mirroring ECMA-262 NonBreakingSpace = 0x00a0, @@ -144,15 +144,24 @@ export const enum CharCode { Tilde = 0x7e, } -/** Does not include line breaks. For that, see isWhiteSpaceLike. */ -export function isWhiteSpaceSingleLine(ch: number): boolean { - // Note: nextLine is in the Zs space, and should be considered to be a whitespace. - // It is explicitly not a line-break as it isn't in the exact set specified by EcmaScript. +export function isAsciiLineBreak(ch: number) { + return ch === CharCode.LineFeed || ch == CharCode.CarriageReturn; +} + +export function isAsciiWhiteSpaceSingleLine(ch: number): boolean { return ( ch === CharCode.Space || ch === CharCode.Tab || ch === CharCode.VerticalTab || - ch === CharCode.FormFeed || + ch === CharCode.FormFeed + ); +} + +export function isNonAsciiWhiteSpaceSingleLine(ch: number) { + // Note: nextLine is in the Zs space, and should be considered to be a + // whitespace. It is explicitly not a line-break as it isn't in the exact set + // inherited by ADL from JavaScript. + return ( ch === CharCode.NonBreakingSpace || ch === CharCode.NextLine || ch === CharCode.Ogham || @@ -164,17 +173,23 @@ export function isWhiteSpaceSingleLine(ch: number): boolean { ); } -export function isLineBreak(ch: number): boolean { - // Other new line or line - // breaking characters are treated as white space but not as line terminators. +export function isNonAsciiLineBreak(ch: number) { + // Other new line or line breaking characters are treated as white space but + // not as line terminators. + return ch === CharCode.ParagraphSeparator || ch === CharCode.LineSeparator; +} + +export function isWhiteSpaceSingleLine(ch: number) { return ( - ch === CharCode.LineFeed || - ch === CharCode.CarriageReturn || - ch === CharCode.LineSeparator || - ch === CharCode.ParagraphSeparator + isAsciiWhiteSpaceSingleLine(ch) || + (ch > CharCode.MaxAscii && isNonAsciiWhiteSpaceSingleLine(ch)) ); } +export function isLineBreak(ch: number): boolean { + return isAsciiLineBreak(ch) || (ch > CharCode.MaxAscii && isNonAsciiLineBreak(ch)); +} + export function isDigit(ch: number): boolean { return ch >= CharCode._0 && ch <= CharCode._9; } @@ -210,7 +225,7 @@ export function isAsciiIdentifierContinue(ch: number): boolean { export function isIdentifierContinue(codePoint: number) { return ( - isAsciiIdentifierStart(codePoint) || + isAsciiIdentifierContinue(codePoint) || (codePoint > CharCode.MaxAscii && isNonAsciiIdentifierContinue(codePoint)) ); } diff --git a/packages/adl/compiler/diagnostics.ts b/packages/adl/compiler/diagnostics.ts index 128aac1ff..e1df47b53 100644 --- a/packages/adl/compiler/diagnostics.ts +++ b/packages/adl/compiler/diagnostics.ts @@ -1,5 +1,5 @@ import { AssertionError } from "assert"; -import { CharCode } from "./charcode.js"; +import { CharCode, isNonAsciiLineBreak } from "./charcode.js"; import { Message } from "./messages.js"; import { Diagnostic, Node, SourceFile, SourceLocation, Sym, SyntaxKind, Type } from "./types.js"; @@ -113,7 +113,7 @@ export function createSourceFile(text: string, path: string): SourceFile { }; function getLineStarts() { - return (lineStarts = lineStarts ?? scanLineStarts()); + return (lineStarts = lineStarts ?? scanLineStarts(text)); } function getLineAndCharacterOfPosition(position: number) { @@ -136,57 +136,6 @@ export function createSourceFile(text: string, path: string): SourceFile { character: position - starts[line], }; } - - function scanLineStarts() { - const starts = []; - let start = 0; - let pos = 0; - - while (pos < text.length) { - const ch = text.charCodeAt(pos); - pos++; - switch (ch) { - case CharCode.CarriageReturn: - if (text.charCodeAt(pos) === CharCode.LineFeed) { - pos++; - } - // fallthrough - case CharCode.LineFeed: - case CharCode.LineSeparator: - case CharCode.ParagraphSeparator: - starts.push(start); - start = pos; - break; - } - } - - starts.push(start); - return starts; - } - - /** - * Search sorted array of numbers for the given value. If found, return index - * in array where value was found. If not found, return a negative number that - * is the bitwise complement of the index where value would need to be inserted - * to keep the array sorted. - */ - function binarySearch(array: readonly number[], value: number) { - let low = 0; - let high = array.length - 1; - while (low <= high) { - const middle = low + ((high - low) >> 1); - const v = array[middle]; - if (v < value) { - low = middle + 1; - } else if (v > value) { - high = middle - 1; - } else { - return middle; - } - } - - return ~low; - } } export function getSourceLocation(target: DiagnosticTarget): SourceLocation { @@ -328,3 +277,58 @@ function format(text: string, args?: (string | number)[]): [string, Error?] { function isNotUndefined(value: T | undefined): value is T { return value !== undefined; } + +function scanLineStarts(text: string): number[] { + const starts = []; + let start = 0; + let pos = 0; + + while (pos < text.length) { + const ch = text.charCodeAt(pos); + pos++; + switch (ch) { + case CharCode.CarriageReturn: + if (text.charCodeAt(pos) === CharCode.LineFeed) { + pos++; + } + // fallthrough + case CharCode.LineFeed: + starts.push(start); + start = pos; + break; + default: + if (ch > CharCode.MaxAscii && isNonAsciiLineBreak(ch)) { + starts.push(start); + start = pos; + break; + } + } + } + + starts.push(start); + return starts; +} + +/** + * Search sorted array of numbers for the given value. If found, return index + * in array where value was found. If not found, return a negative number that + * is the bitwise complement of the index where value would need to be inserted + * to keep the array sorted. + */ +function binarySearch(array: readonly number[], value: number) { + let low = 0; + let high = array.length - 1; + while (low <= high) { + const middle = low + ((high - low) >> 1); + const v = array[middle]; + if (v < value) { + low = middle + 1; + } else if (v > value) { + high = middle - 1; + } else { + return middle; + } + } + + return ~low; +} diff --git a/packages/adl/compiler/messages.ts b/packages/adl/compiler/messages.ts index 0ff64ea6c..b561d711c 100644 --- a/packages/adl/compiler/messages.ts +++ b/packages/adl/compiler/messages.ts @@ -8,50 +8,50 @@ export const Message = { DigitExpected: { code: 1100, severity: "error", - text: "Digit expected (0-9)", + text: "Digit expected.", } as const, HexDigitExpected: { code: 1101, severity: "error", - text: "Hex Digit expected (0-F)", + text: "Hexadecimal digit expected.", } as const, BinaryDigitExpected: { code: 1102, severity: "error", - text: "Binary Digit expected (0,1)", + text: "Binary digit expected.", } as const, - UnexpectedEndOfFile: { + Unterminated: { code: 1103, severity: "error", - text: "Unexpected end of file while searching for '{0}'", + text: "Unterminated {0}.", } as const, InvalidEscapeSequence: { code: 1104, severity: "error", - text: "Invalid escape sequence", + text: "Invalid escape sequence.", } as const, NoNewLineAtStartOfTripleQuotedString: { code: 1105, severity: "error", - text: "String content in triple quotes must begin on a new line", + text: "String content in triple quotes must begin on a new line.", } as const, NoNewLineAtEndOfTripleQuotedString: { code: 1106, severity: "error", - text: "Closing triple quotes must begin on a new line", + text: "Closing triple quotes must begin on a new line.", } as const, InconsistentTripleQuoteIndentation: { code: 1107, severity: "error", text: - "All lines in triple-quoted string lines must have the same indentation as closing triple quotes", + "All lines in triple-quoted string lines must have the same indentation as closing triple quotes.", } as const, InvalidCharacter: { diff --git a/packages/adl/compiler/parser.ts b/packages/adl/compiler/parser.ts index bd0209974..2183e255b 100644 --- a/packages/adl/compiler/parser.ts +++ b/packages/adl/compiler/parser.ts @@ -280,7 +280,7 @@ export function parse(code: string | SourceFile) { stmts.push(parseUsingStatement()); break; case Token.EndOfFile: - error("End of file reached without '}'."); + parseExpected(Token.CloseBrace); return stmts; case Token.Semicolon: reportInvalidDecorators(decorators, "empty statement"); @@ -990,9 +990,8 @@ export function parse(code: string | SourceFile) { if (realPositionOfLastError === realPos) { return; } - realPositionOfLastError = realPos; - parseErrorInNextFinishedNode = true; + reportDiagnostic(message, location); } @@ -1001,6 +1000,9 @@ export function parse(code: string | SourceFile) { target: DiagnosticTarget, args?: (string | number)[] ) { + if (typeof message === "string" || message.severity === "error") { + parseErrorInNextFinishedNode = true; + } const diagnostic = createDiagnostic(message, target, args); parseDiagnostics.push(diagnostic); } diff --git a/packages/adl/compiler/scanner.ts b/packages/adl/compiler/scanner.ts index a0f5790dd..73db4a65e 100644 --- a/packages/adl/compiler/scanner.ts +++ b/packages/adl/compiler/scanner.ts @@ -9,6 +9,8 @@ import { isLineBreak, isNonAsciiIdentifierContinue, isNonAsciiIdentifierStart, + isNonAsciiLineBreak, + isNonAsciiWhiteSpaceSingleLine, isWhiteSpaceSingleLine, } from "./charcode.js"; import { createSourceFile, Message, throwOnError } from "./diagnostics.js"; @@ -82,6 +84,7 @@ const MaxPunctuation = Token.At; const MinStatementKeyword = Token.ImportKeyword; const MaxStatementKeyword = Token.OpKeyword; +/** @internal */ export const TokenDisplay: readonly string[] = [ "", "", @@ -122,6 +125,7 @@ export const TokenDisplay: readonly string[] = [ "'false'", ]; +/** @internal */ export const Keywords: ReadonlyMap = new Map([ ["import", Token.ImportKeyword], ["model", Token.ModelKeyword], @@ -133,7 +137,13 @@ export const Keywords: ReadonlyMap = new Map([ ["false", Token.FalseKeyword], ]); -export const maxKeywordLength = 9; +/** @internal */ +export const enum KeywordLimit { + MinLength = 2, + MaxLength = 9, + MinStartChar = CharCode.e, + MaxStartChar = CharCode.u, +} export interface Scanner { /** The source code being scanned. */ @@ -264,32 +274,12 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro } // fallthrough case CharCode.LineFeed: - case CharCode.LineSeparator: - case CharCode.ParagraphSeparator: return next(Token.NewLine); + case CharCode.Space: case CharCode.Tab: case CharCode.VerticalTab: case CharCode.FormFeed: - case CharCode.Space: - case CharCode.NonBreakingSpace: - case CharCode.Ogham: - case CharCode.EnQuad: - case CharCode.EmQuad: - case CharCode.EnSpace: - case CharCode.EmSpace: - case CharCode.ThreePerEmSpace: - case CharCode.FourPerEmSpace: - case CharCode.SixPerEmSpace: - case CharCode.FigureSpace: - case CharCode.PunctuationSpace: - case CharCode.ThinSpace: - case CharCode.HairSpace: - case CharCode.ZeroWidthSpace: - case CharCode.NarrowNoBreakSpace: - case CharCode.MathematicalSpace: - case CharCode.IdeographicSpace: - case CharCode.ByteOrderMark: return scanWhitespace(); case CharCode.OpenParen: @@ -382,16 +372,45 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro : next(Token.Bar); case CharCode.DoubleQuote: - return scanString(); + return lookAhead(1) === CharCode.DoubleQuote && lookAhead(2) === CharCode.DoubleQuote + ? scanTripleQuotedString() + : scanString(); default: - return scanIdentifierOrKeyword(); + if (isAsciiIdentifierStart(ch)) { + return scanIdentifierOrKeyword(); + } + + if (ch <= CharCode.MaxAscii) { + return scanInvalidCharacter(); + } + + return scanNonAsciiToken(); } } return (token = Token.EndOfFile); } + function scanNonAsciiToken() { + const ch = input.charCodeAt(position); + + if (isNonAsciiLineBreak(ch)) { + return next(Token.NewLine); + } + + if (isNonAsciiWhiteSpaceSingleLine(ch)) { + return scanWhitespace(); + } + + const codePoint = input.codePointAt(position)!; + if (isNonAsciiIdentifierStart(codePoint)) { + return scanNonAsciiIdentifierContinue(codePoint); + } + + return scanInvalidCharacter(); + } + function scanInvalidCharacter() { const codePoint = input.codePointAt(position)!; token = next(Token.Invalid, utf16CodeUnits(codePoint)); @@ -423,152 +442,184 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro function scanWhitespace(): Token { do { position++; - } while (isWhiteSpaceSingleLine(input.charCodeAt(position))); + } while (!eof() && isWhiteSpaceSingleLine(input.charCodeAt(position))); return (token = Token.Whitespace); } - function scanDigits() { - while (isDigit(input.charCodeAt(position))) { - position++; - } - } - function scanNumber() { - scanDigits(); - - let ch = input.charCodeAt(position); - - if (ch === CharCode.Dot) { - position++; - scanDigits(); - } - - ch = input.charCodeAt(position); - if (ch === CharCode.e) { - position++; - ch = input.charCodeAt(position); - if (ch === CharCode.Plus || ch == CharCode.Minus) { - position++; - ch = input.charCodeAt(position); - } - - if (isDigit(ch)) { - position++; - scanDigits(); - } else { - error(Message.DigitExpected); + scanKnownDigits(); + if (!eof()) { + switch (input.charCodeAt(position)) { + case CharCode.Dot: + scanFractionAndExponent(); + break; + case CharCode.e: + scanExponent(); + break; } } - return (token = Token.NumericLiteral); } - function scanHexNumber() { - if (!isHexDigit(lookAhead(2))) { - error(Message.HexDigitExpected); - return next(Token.NumericLiteral, 2); - } + function scanKnownDigits() { + do { + position++; + } while (!eof() && isDigit(input.charCodeAt(position))); + } + + function scanOptionalDigits() { + if (!eof() && isDigit(input.charCodeAt(position))) { + scanKnownDigits(); + } + } + + function scanRequiredDigits() { + if (eof() || !isDigit(input.charCodeAt(position))) { + error(Message.DigitExpected); + return; + } + scanKnownDigits(); + } + + function scanFractionAndExponent() { + position++; // consume '.' + scanOptionalDigits(); + if (!eof() && input.charCodeAt(position) === CharCode.e) { + scanExponent(); + } + } + + function scanExponent() { + position++; // consume 'e' + if (eof()) { + error(Message.DigitExpected); + return; + } + const ch = input.charCodeAt(position); + if (ch === CharCode.Plus || ch === CharCode.Minus) { + position++; + } + scanRequiredDigits(); + } + + function scanHexNumber() { + position += 2; // consume '0x' + + if (eof() || !isHexDigit(input.charCodeAt(position))) { + error(Message.HexDigitExpected); + return (token = Token.NumericLiteral); + } + do { + position++; + } while (!eof() && isHexDigit(input.charCodeAt(position))); - position += 2; - scanUntil((ch) => !isHexDigit(ch), "Hex Digit"); return (token = Token.NumericLiteral); } function scanBinaryNumber() { - if (!isBinaryDigit(lookAhead(2))) { - error(Message.BinaryDigitExpected); - return next(Token.NumericLiteral, 2); - } + position += 2; // consume '0b' + + if (eof() || !isBinaryDigit(input.charCodeAt(position))) { + error(Message.BinaryDigitExpected); + return (token = Token.NumericLiteral); + } + do { + position++; + } while (!eof() && isBinaryDigit(input.charCodeAt(position))); - position += 2; - scanUntil((ch) => !isBinaryDigit(ch), "Binary Digit"); return (token = Token.NumericLiteral); } - function scanUntil( - predicate: (char: number) => boolean, - expectedClose?: string, - consumeClose?: number - ) { - let ch: number; + function scanSingleLineComment() { + position += 2; // consume '//' - do { - position++; - - if (eof()) { - if (expectedClose) { - error(Message.UnexpectedEndOfFile, [expectedClose]); - } + while (!eof()) { + if (isLineBreak(input.charCodeAt(position))) { break; } - - ch = input.charCodeAt(position); - } while (!predicate(ch)); - - if (consumeClose) { - position += consumeClose; + position++; } - } - function scanSingleLineComment() { - scanUntil(isLineBreak); return (token = Token.SingleLineComment); } function scanMultiLineComment() { - scanUntil((ch) => ch === CharCode.Asterisk && lookAhead(1) === CharCode.Slash, "*/", 2); + position += 2; // consume '/*' + + while (!eof()) { + if (input.charCodeAt(position) === CharCode.Asterisk && lookAhead(1) === CharCode.Slash) { + position += 2; + return (token = Token.MultiLineComment); + } + position++; + } + + error(Message.Unterminated, ["comment"]); return (token = Token.MultiLineComment); } function scanString() { - let quoteLength = 1; - let closing = '"'; - let isEscaping = false; + position++; // consume '"' - const tripleQuoted = - lookAhead(1) === CharCode.DoubleQuote && lookAhead(2) === CharCode.DoubleQuote; - - if (tripleQuoted) { - tokenFlags |= TokenFlags.TripleQuoted; - quoteLength = 3; - position += 2; - closing = '"""'; + loop: while (!eof()) { + const ch = input.charCodeAt(position); + switch (ch) { + case CharCode.CarriageReturn: + if (lookAhead(1) === CharCode.LineFeed) { + tokenFlags |= TokenFlags.HasCrlf; + position++; + } + break; + case CharCode.Backslash: + tokenFlags |= TokenFlags.Escaped; + position++; + if (eof()) { + break loop; + } + break; + case CharCode.DoubleQuote: + position++; + return (token = Token.StringLiteral); + } + position++; } - scanUntil( - (ch) => { - if (isEscaping) { - isEscaping = false; - return false; - } + error(Message.Unterminated, ["string literal"]); + return (token = Token.StringLiteral); + } - switch (ch) { - case CharCode.CarriageReturn: - if (lookAhead(1) === CharCode.LineFeed) { - tokenFlags |= TokenFlags.HasCrlf; - } - return false; + function scanTripleQuotedString() { + tokenFlags |= TokenFlags.TripleQuoted; + position += 3; // consume '"""' - case CharCode.Backslash: - isEscaping = true; - tokenFlags |= TokenFlags.Escaped; - return false; - - case CharCode.DoubleQuote: - if (tripleQuoted) { - return lookAhead(1) === CharCode.DoubleQuote && lookAhead(2) === CharCode.DoubleQuote; - } - return true; - - default: - return false; - } - }, - closing, - quoteLength - ); + loop: while (!eof()) { + const ch = input.charCodeAt(position); + switch (ch) { + case CharCode.CarriageReturn: + if (lookAhead(1) === CharCode.LineFeed) { + tokenFlags |= TokenFlags.HasCrlf; + position++; + } + break; + case CharCode.Backslash: + tokenFlags |= TokenFlags.Escaped; + position++; + if (eof()) { + break loop; + } + break; + case CharCode.DoubleQuote: + if (lookAhead(1) === CharCode.DoubleQuote && lookAhead(2) === CharCode.DoubleQuote) { + position += 3; + return (token = Token.StringLiteral); + } + break; + } + position++; + } + error(Message.Unterminated, ["string literal"]); return (token = Token.StringLiteral); } @@ -576,11 +627,10 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro if (tokenValue !== undefined) { return tokenValue; } + return (tokenValue = token === Token.StringLiteral ? getStringTokenValue() : getTokenText()); + } - if (token !== Token.StringLiteral) { - return (tokenValue = getTokenText()); - } - + function getStringTokenValue() { // strip quotes const quoteLength = tokenFlags & TokenFlags.TripleQuoted ? 3 : 1; let value = input.substring(tokenPosition + quoteLength, position - quoteLength); @@ -729,30 +779,28 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro } function scanIdentifierOrKeyword() { - let ch = input.charCodeAt(position); - - if (!isAsciiIdentifierStart(ch)) { - return scanNonAsciiIdentifier(); - } - + const startChar = input.charCodeAt(position); + let ch = startChar; do { position++; - if (eof()) { - break; - } - ch = input.charCodeAt(position); - } while (isAsciiIdentifierContinue(ch)); + } while (!eof() && isAsciiIdentifierContinue((ch = input.charCodeAt(position)))); - if (!eof() && ch > CharCode.MaxAscii) { + if (ch > CharCode.MaxAscii) { const codePoint = input.codePointAt(position)!; if (isNonAsciiIdentifierContinue(codePoint)) { return scanNonAsciiIdentifierContinue(codePoint); } } - if (position - tokenPosition <= maxKeywordLength) { - const value = getTokenValue(); - const keyword = Keywords.get(value); + const length = position - tokenPosition; + if ( + length >= KeywordLimit.MinLength && + length <= KeywordLimit.MaxLength && + startChar >= KeywordLimit.MinStartChar && + startChar <= KeywordLimit.MaxStartChar + ) { + tokenValue = getTokenText(); + const keyword = Keywords.get(tokenValue); if (keyword) { return (token = keyword); } @@ -761,23 +809,11 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro return (token = Token.Identifier); } - function scanNonAsciiIdentifier() { - let codePoint = input.codePointAt(position)!; - return isNonAsciiIdentifierStart(codePoint) - ? scanNonAsciiIdentifierContinue(codePoint) - : scanInvalidCharacter(); - } - function scanNonAsciiIdentifierContinue(startCodePoint: number) { let codePoint = startCodePoint; - do { position += utf16CodeUnits(codePoint); - if (eof()) { - break; - } - codePoint = input.codePointAt(position)!; - } while (isIdentifierContinue(codePoint)); + } while (!eof() && isIdentifierContinue((codePoint = input.codePointAt(position)!))); return (token = Token.Identifier); } diff --git a/packages/adl/test/test-parser.ts b/packages/adl/test/test-parser.ts index e0878f57c..33ac5489d 100644 --- a/packages/adl/test/test-parser.ts +++ b/packages/adl/test/test-parser.ts @@ -219,8 +219,38 @@ describe("syntax", () => { ]); }); + describe("unterminated tokens", () => { + parseErrorEach([ + ['model X = "banana', [/Unterminated string literal/]], + ['model X = "banana\\', [/Unterminated string literal/]], + ['model X = """\nbanana', [/Unterminated string literal/]], + ['model X = """\nbanana\\', [/Unterminated string literal/]], + ["/* Yada yada yada", [/Unterminated comment/]], + ["123.0e", [/Digit expected/]], + ["123.e", [/Digit expected/]], + ["123e", [/Digit expected/]], + ["0b", [/Binary digit expected/]], + ["0x", [/Hexadecimal digit expected/]], + ]); + }); + + describe("terminated tokens at EOF with missing semicolon", () => { + parseErrorEach([ + ["model X = 0x10101", [/';' expected/]], + ["model X = 0xBEEF", [/';' expected/]], + ["model X = 123", [/';' expected/]], + ["model X = 123.", [/';' expected/]], + ["model X = 123e45", [/';' expected/]], + ["model X = 123.45", [/';' expected/]], + ["model X = 123.45e2", [/';' expected/]], + ["model X = Banana", [/';' expected/]], + ['model X = "Banana"', [/';' expected/]], + ['model X = """\nBanana\n"""', [/';' expected/]], + ]); + }); + describe("non-ascii identifiers", () => { - parseEach(["model Incompréhensible {}", "model 𐌰𐌲 {}", "model Banana𐌰𐌲Banana {}"]); + parseEach(["model Incompréhensible {}", "model 𐌰𐌲 {}", "model Banana𐌰𐌲42Banana {}"]); parseErrorEach([["model 😢 {}", [/Invalid character/]]]); }); }); diff --git a/packages/adl/test/test-scanner.ts b/packages/adl/test/test-scanner.ts index f81c4a415..68fea0ff8 100644 --- a/packages/adl/test/test-scanner.ts +++ b/packages/adl/test/test-scanner.ts @@ -7,14 +7,22 @@ import { isKeyword, isPunctuation, isStatementKeyword, + KeywordLimit, Keywords, - maxKeywordLength, Token, TokenDisplay, } from "../compiler/scanner.js"; -import { LineAndCharacter } from "../compiler/types.js"; -type TokenEntry = [Token, string?, number?, LineAndCharacter?]; +type TokenEntry = [ + Token, + string?, + { + pos?: number; + line?: number; + character?: number; + value?: string; + }? +]; function tokens(text: string, onError = throwOnError): TokenEntry[] { const scanner = createScanner(text, onError); @@ -25,8 +33,11 @@ function tokens(text: string, onError = throwOnError): TokenEntry[] { result.push([ scanner.token, scanner.getTokenText(), - scanner.tokenPosition, - scanner.file.getLineAndCharacterOfPosition(scanner.tokenPosition), + { + pos: scanner.tokenPosition, + value: scanner.getTokenValue(), + ...scanner.file.getLineAndCharacterOfPosition(scanner.tokenPosition), + }, ]); } while (!scanner.eof()); @@ -38,26 +49,43 @@ function tokens(text: string, onError = throwOnError): TokenEntry[] { } function verify(tokens: TokenEntry[], expecting: TokenEntry[]) { - for (const [ - index, - [expectedToken, expectedText, expectedPosition, expectedLineAndCharacter], - ] of expecting.entries()) { - const [token, text, position, lineAndCharacter] = tokens[index]; + for (const [index, [expectedToken, expectedText, expectedAdditional]] of expecting.entries()) { + const [token, text, additional] = tokens[index]; assert.strictEqual(Token[token], Token[expectedToken], `Token ${index} must match`); if (expectedText) { assert.strictEqual(text, expectedText, `Token ${index} test must match`); } - if (expectedPosition) { - assert.strictEqual(position, expectedPosition, `Token ${index} position must match`); + if (expectedAdditional?.pos) { + assert.strictEqual( + additional!.pos, + expectedAdditional.pos, + `Token ${index} position must match` + ); } - if (expectedLineAndCharacter) { - assert.deepStrictEqual( - lineAndCharacter, - expectedLineAndCharacter, - `Token ${index} line and character must match` + if (expectedAdditional?.line) { + assert.strictEqual( + additional!.line, + expectedAdditional.line, + `Token ${index} line must match` + ); + } + + if (expectedAdditional?.character) { + assert.strictEqual( + additional!.character, + expectedAdditional?.character, + `Token ${index} character must match` + ); + } + + if (expectedAdditional?.value) { + assert.strictEqual( + additional!.value, + expectedAdditional.value, + `Token ${index} value must match` ); } } @@ -66,16 +94,16 @@ function verify(tokens: TokenEntry[], expecting: TokenEntry[]) { describe("scanner", () => { /** verifies that we can scan tokens and get back some output. */ it("smoketest", () => { - const all = tokens("\tthis is a test"); + const all = tokens('\tthis is "a" test'); verify(all, [ [Token.Whitespace], - [Token.Identifier, "this"], + [Token.Identifier, "this", { value: "this" }], [Token.Whitespace], - [Token.Identifier, "is"], + [Token.Identifier, "is", { value: "is" }], [Token.Whitespace], - [Token.Identifier, "a"], + [Token.StringLiteral, '"a"', { value: "a" }], [Token.Whitespace], - [Token.Identifier, "test"], + [Token.Identifier, "test", { value: "test" }], ]); }); @@ -130,7 +158,7 @@ describe("scanner", () => { }); it("scans numeric literals", () => { - const all = tokens("42 0xBEEF 0b1010 1.5e4 314.0e-2 1e+1000"); + const all = tokens("42 0xBEEF 0b1010 1.5e4 314.0e-2 1e+1000 3. 2.e3"); verify(all, [ [Token.NumericLiteral, "42"], [Token.Whitespace], @@ -143,6 +171,11 @@ describe("scanner", () => { [Token.NumericLiteral, "314.0e-2"], [Token.Whitespace], [Token.NumericLiteral, "1e+1000"], + [Token.Whitespace], + // https://github.com/Azure/adl/issues/488 - we may want to disallow these + [Token.NumericLiteral, "3."], + [Token.Whitespace], + [Token.NumericLiteral, "2.e3"], ]); }); @@ -184,34 +217,34 @@ describe("scanner", () => { it("provides token position", () => { const all = tokens("a x\raa x\r\naaa x\naaaa x\u{2028}aaaaa x\u{2029}aaaaaa x"); verify(all, [ - [Token.Identifier, "a", 0, { line: 0, character: 0 }], - [Token.Whitespace, " ", 1, { line: 0, character: 1 }], - [Token.Identifier, "x", 2, { line: 0, character: 2 }], - [Token.NewLine, "\r", 3, { line: 0, character: 3 }], + [Token.Identifier, "a", { pos: 0, line: 0, character: 0 }], + [Token.Whitespace, " ", { pos: 1, line: 0, character: 1 }], + [Token.Identifier, "x", { pos: 2, line: 0, character: 2 }], + [Token.NewLine, "\r", { pos: 3, line: 0, character: 3 }], - [Token.Identifier, "aa", 4, { line: 1, character: 0 }], - [Token.Whitespace, " ", 6, { line: 1, character: 2 }], - [Token.Identifier, "x", 7, { line: 1, character: 3 }], - [Token.NewLine, "\r\n", 8, { line: 1, character: 4 }], + [Token.Identifier, "aa", { pos: 4, line: 1, character: 0 }], + [Token.Whitespace, " ", { pos: 6, line: 1, character: 2 }], + [Token.Identifier, "x", { pos: 7, line: 1, character: 3 }], + [Token.NewLine, "\r\n", { pos: 8, line: 1, character: 4 }], - [Token.Identifier, "aaa", 10, { line: 2, character: 0 }], - [Token.Whitespace, " ", 13, { line: 2, character: 3 }], - [Token.Identifier, "x", 14, { line: 2, character: 4 }], - [Token.NewLine, "\n", 15, { line: 2, character: 5 }], + [Token.Identifier, "aaa", { pos: 10, line: 2, character: 0 }], + [Token.Whitespace, " ", { pos: 13, line: 2, character: 3 }], + [Token.Identifier, "x", { pos: 14, line: 2, character: 4 }], + [Token.NewLine, "\n", { pos: 15, line: 2, character: 5 }], - [Token.Identifier, "aaaa", 16, { line: 3, character: 0 }], - [Token.Whitespace, " ", 20, { line: 3, character: 4 }], - [Token.Identifier, "x", 21, { line: 3, character: 5 }], - [Token.NewLine, "\u{2028}", 22, { line: 3, character: 6 }], + [Token.Identifier, "aaaa", { pos: 16, line: 3, character: 0 }], + [Token.Whitespace, " ", { pos: 20, line: 3, character: 4 }], + [Token.Identifier, "x", { pos: 21, line: 3, character: 5 }], + [Token.NewLine, "\u{2028}", { pos: 22, line: 3, character: 6 }], - [Token.Identifier, "aaaaa", 23, { line: 4, character: 0 }], - [Token.Whitespace, " ", 28, { line: 4, character: 5 }], - [Token.Identifier, "x", 29, { line: 4, character: 6 }], - [Token.NewLine, "\u{2029}", 30, { line: 4, character: 7 }], + [Token.Identifier, "aaaaa", { pos: 23, line: 4, character: 0 }], + [Token.Whitespace, " ", { pos: 28, line: 4, character: 5 }], + [Token.Identifier, "x", { pos: 29, line: 4, character: 6 }], + [Token.NewLine, "\u{2029}", { pos: 30, line: 4, character: 7 }], - [Token.Identifier, "aaaaaa", 31, { line: 5, character: 0 }], - [Token.Whitespace, " ", 37, { line: 5, character: 6 }], - [Token.Identifier, "x", 38, { line: 5, character: 7 }], + [Token.Identifier, "aaaaaa", { pos: 31, line: 5, character: 0 }], + [Token.Whitespace, " ", { pos: 37, line: 5, character: 6 }], + [Token.Identifier, "x", { pos: 38, line: 5, character: 7 }], ]); }); @@ -225,11 +258,19 @@ describe("scanner", () => { `Token enum has ${tokenCount} elements but TokenDisplay array has ${tokenDisplayCount}.` ); - // check that keywords have appropriate display + // check that keywords have appropriate display and limits const nonStatementKeywords = [Token.ExtendsKeyword, Token.TrueKeyword, Token.FalseKeyword]; - let maxKeywordLengthFound = -1; + let minKeywordLengthFound = Number.MAX_SAFE_INTEGER; + let maxKeywordLengthFound = Number.MIN_SAFE_INTEGER; + let minKeywordStartCharFound = Number.MAX_SAFE_INTEGER; + let maxKeywordStartCharFound = Number.MIN_SAFE_INTEGER; + for (const [name, token] of Keywords.entries()) { + minKeywordLengthFound = Math.min(minKeywordLengthFound, name.length); maxKeywordLengthFound = Math.max(maxKeywordLengthFound, name.length); + minKeywordStartCharFound = Math.min(minKeywordStartCharFound, name.charCodeAt(0)); + maxKeywordStartCharFound = Math.max(maxKeywordStartCharFound, name.charCodeAt(0)); + assert.strictEqual(TokenDisplay[token], `'${name}'`); assert(isKeyword(token), `${name} should be classified as a keyword`); if (!nonStatementKeywords.includes(token)) { @@ -237,7 +278,10 @@ describe("scanner", () => { } } - assert.strictEqual(maxKeywordLengthFound, maxKeywordLength); + assert.strictEqual(minKeywordLengthFound, KeywordLimit.MinLength); + assert.strictEqual(maxKeywordLengthFound, KeywordLimit.MaxLength); + assert.strictEqual(minKeywordStartCharFound, KeywordLimit.MinStartChar); + assert.strictEqual(maxKeywordStartCharFound, KeywordLimit.MaxStartChar); // check single character punctuation for (let i = 33; i <= 126; i++) { From 3ac9719b179c24528df9a38d820faada55324baf Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sat, 24 Apr 2021 18:45:40 -0700 Subject: [PATCH 09/47] Allow ZWJ and ZWNJ to continue identifiers as specified --- packages/adl/compiler/charcode.ts | 7 ++++ packages/adl/compiler/nonascii.ts | 15 ++++++-- packages/adl/scripts/regen-nonascii.js | 21 ++++++++--- packages/adl/test/test-parser.ts | 8 ++++- packages/adl/test/test-scanner.ts | 49 ++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/packages/adl/compiler/charcode.ts b/packages/adl/compiler/charcode.ts index 7545b3f32..94b6e7d13 100644 --- a/packages/adl/compiler/charcode.ts +++ b/packages/adl/compiler/charcode.ts @@ -223,6 +223,13 @@ export function isAsciiIdentifierContinue(ch: number): boolean { ); } +export function isIdentifierStart(codePoint: number) { + return ( + isAsciiIdentifierStart(codePoint) || + (codePoint > CharCode.MaxAscii && isNonAsciiIdentifierStart(codePoint)) + ); +} + export function isIdentifierContinue(codePoint: number) { return ( isAsciiIdentifierContinue(codePoint) || diff --git a/packages/adl/compiler/nonascii.ts b/packages/adl/compiler/nonascii.ts index 8f8d19b33..a20ca06ab 100644 --- a/packages/adl/compiler/nonascii.ts +++ b/packages/adl/compiler/nonascii.ts @@ -4,15 +4,19 @@ // // Based on: // - http://www.unicode.org/reports/tr31/ -// - https://www.ecma-international.org/ecma-262/6.0/#sec-names-and-keywords +// - https://www.ecma-international.org/ecma-262/11.0/#sec-names-and-keywords // // ADL's identifier naming rules are currently the same as JavaScript's. +// /** * @internal * * Map of non-ascii characters that are valid at the start of an identifier. * Each pair of numbers represents an inclusive range of code points. + * + * Corresponds to code points outside the ASCII range with property ID_Start or + * Other_ID_Start. */ // prettier-ignore export const nonAsciiIdentifierStartMap: readonly number[] = [ @@ -641,8 +645,12 @@ export const nonAsciiIdentifierStartMap: readonly number[] = [ /** * @internal * - * Map of non-ascii chacters that are valid after the first character in and identifier. - * Each pair of numbers represents an inclusive range of code points. + * Map of non-ascii chacters that are valid after the first character in and + * identifier. Each pair of numbers represents an inclusive range of code + * points. + * + * Corresponds to code points outside the ASCII range with property ID_Continue, + * Other_ID_Start, or Other_ID_Continue, plus ZWNJ and ZWJ. */ //prettier-ignore export const nonAsciiIdentifierContinueMap: readonly number[] = [ @@ -943,6 +951,7 @@ export const nonAsciiIdentifierContinueMap: readonly number[] = [ 0x1fe0, 0x1fec, 0x1ff2, 0x1ff4, 0x1ff6, 0x1ffc, + 0x200c, 0x200d, 0x203f, 0x2040, 0x2054, 0x2054, 0x2071, 0x2071, diff --git a/packages/adl/scripts/regen-nonascii.js b/packages/adl/scripts/regen-nonascii.js index a85d5b7f3..65ba2d62c 100644 --- a/packages/adl/scripts/regen-nonascii.js +++ b/packages/adl/scripts/regen-nonascii.js @@ -9,8 +9,11 @@ import { fileURLToPath } from "url"; const MIN_NONASCII_CODEPOINT = 0x80; const MAX_UNICODE_CODEPOINT = 0x10ffff; -const isStartRegex = /[\p{ID_Start}\u{2118}\u{212E}\u{309B}\u{309C}]/u; -const isContinueRegex = /[\p{ID_Continue}\u{00B7}\u{0387}\u{19DA}\u{1369}\u{136A}\u{136B}\u{136C}\u{136D}\u{136E}\u{136F}\u{1370}\u{1371}]/u; +// Includes Other_ID_Start +const isStartRegex = /[\p{ID_Start}]/u; + +// Includes Other_ID_Start and Other_ID_Continue +const isContinueRegex = /[\p{ID_Continue}\u{200c}\u{200d}]/u; function isStart(c) { return isStartRegex.test(c); @@ -50,15 +53,19 @@ const src = `// // // Based on: // - http://www.unicode.org/reports/tr31/ -// - https://www.ecma-international.org/ecma-262/6.0/#sec-names-and-keywords +// - https://www.ecma-international.org/ecma-262/11.0/#sec-names-and-keywords // // ADL's identifier naming rules are currently the same as JavaScript's. +// /** * @internal * * Map of non-ascii characters that are valid at the start of an identifier. * Each pair of numbers represents an inclusive range of code points. + * + * Corresponds to code points outside the ASCII range with property ID_Start or + * Other_ID_Start. */ // prettier-ignore export const nonAsciiIdentifierStartMap: readonly number[] = [ @@ -68,8 +75,12 @@ ${formatPairs(startMap)} /** * @internal * - * Map of non-ascii chacters that are valid after the first character in and identifier. - * Each pair of numbers represents an inclusive range of code points. + * Map of non-ascii chacters that are valid after the first character in and + * identifier. Each pair of numbers represents an inclusive range of code + * points. + * + * Corresponds to code points outside the ASCII range with property ID_Continue, + * Other_ID_Start, or Other_ID_Continue, plus ZWNJ and ZWJ. */ //prettier-ignore export const nonAsciiIdentifierContinueMap: readonly number[] = [ diff --git a/packages/adl/test/test-parser.ts b/packages/adl/test/test-parser.ts index 33ac5489d..617fd6e22 100644 --- a/packages/adl/test/test-parser.ts +++ b/packages/adl/test/test-parser.ts @@ -250,7 +250,13 @@ describe("syntax", () => { }); describe("non-ascii identifiers", () => { - parseEach(["model Incompréhensible {}", "model 𐌰𐌲 {}", "model Banana𐌰𐌲42Banana {}"]); + parseEach([ + "model Incompréhensible {}", + "model 𐌰𐌲 {}", + "model Banana𐌰𐌲42Banana {}", + "model deaf\u{200c}ly {}", // ZWNJ + "model क्‍ष {}", // ZWJ + ]); parseErrorEach([["model 😢 {}", [/Invalid character/]]]); }); }); diff --git a/packages/adl/test/test-scanner.ts b/packages/adl/test/test-scanner.ts index 68fea0ff8..5378328e4 100644 --- a/packages/adl/test/test-scanner.ts +++ b/packages/adl/test/test-scanner.ts @@ -1,6 +1,7 @@ import assert from "assert"; import { readFile } from "fs/promises"; import { URL } from "url"; +import { isIdentifierContinue, isIdentifierStart } from "../compiler/charcode.js"; import { throwOnError } from "../compiler/diagnostics.js"; import { createScanner, @@ -311,6 +312,54 @@ describe("scanner", () => { assert.strictEqual(TokenDisplay[Token.Identifier], ""); }); + // Search for Other_ID_Start in https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt + const otherIDStart = [0x1885, 0x1886, 0x2118, 0x212e, 0x309b, 0x309c]; + + // Search for Other_ID_Continue in https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt + const otherIdContinue = [ + 0x00b7, + 0x0387, + 0x1369, + 0x136a, + 0x136b, + 0x136c, + 0x136d, + 0x136e, + 0x136f, + 0x1370, + 0x1371, + 0x19da, + ]; + + it("allows additional identifier start characters", () => { + assert(isIdentifierStart("$".codePointAt(0)!), "'$' should be allowed to start identifier."); + assert(isIdentifierStart("_".codePointAt(0)!), "'_' should be allowed to start identifier."); + + for (const codePoint of otherIDStart) { + assert( + isIdentifierStart(codePoint), + `U+${codePoint.toString(16)} should be allowed to start identifier.` + ); + } + }); + + it("allows additional identifier continuation characters", () => { + //prettier-ignore + assert(isIdentifierContinue("$".codePointAt(0)!), "'$' should be allowed to continue identifier."); + //prettier-ignore + assert(isIdentifierContinue("_".codePointAt(0)!), "'_' should be allowed to continue identifier."); + + for (const codePoint of [...otherIDStart, ...otherIdContinue]) { + assert( + isIdentifierContinue(codePoint), + `U+${codePoint.toString(16)} should be allowed to continue identifier.` + ); + } + + assert(isIdentifierContinue(0x200c), "U+200C (ZWNJ) should be allowed to continue identifier."); + assert(isIdentifierContinue(0x200d), "U+200D (ZWJ) should be allowed to continue identifier."); + }); + it("scans this file", async () => { const text = await readFile(new URL(import.meta.url), "utf-8"); tokens(text, function () { From fa575e515ec3e5ae815a2b19147d63d9c9238927 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sun, 25 Apr 2021 15:10:20 -0700 Subject: [PATCH 10/47] Optimize finishNode calls in parser Spreading the 3 finishing values turns out to be noticeably cheaper than spreading the unfinished node. --- packages/adl/compiler/parser.ts | 360 +++++++++++++------------------- 1 file changed, 150 insertions(+), 210 deletions(-) diff --git a/packages/adl/compiler/parser.ts b/packages/adl/compiler/parser.ts index 2183e255b..463ae5c41 100644 --- a/packages/adl/compiler/parser.ts +++ b/packages/adl/compiler/parser.ts @@ -186,7 +186,7 @@ export function parse(code: string | SourceFile) { locals: createSymbolTable(), inScopeNamespaces: [], parseDiagnostics, - ...finishNode({}, 0), + ...finishNode(0), }; } @@ -316,8 +316,6 @@ export function parse(code: string | SourceFile) { } nsSegments.push(currentName); - let parameters: ModelExpressionNode | undefined; - const nextTok = parseExpectedOneOf(Token.Semicolon, Token.OpenBrace); let statements: Statement[] | undefined; @@ -326,28 +324,22 @@ export function parse(code: string | SourceFile) { parseExpected(Token.CloseBrace); } - let outerNs: NamespaceStatementNode = finishNode( - { - kind: SyntaxKind.NamespaceStatement, - decorators, - name: nsSegments[0], - parameters, - statements, - }, - nsSegments[0].pos - ); + let outerNs: NamespaceStatementNode = { + kind: SyntaxKind.NamespaceStatement, + decorators, + name: nsSegments[0], + statements, + ...finishNode(nsSegments[0].pos), + }; for (let i = 1; i < nsSegments.length; i++) { - outerNs = finishNode( - { - kind: SyntaxKind.NamespaceStatement, - decorators: [], - name: nsSegments[i], - parameters, - statements: outerNs, - }, - nsSegments[i].pos - ); + outerNs = { + kind: SyntaxKind.NamespaceStatement, + decorators: [], + name: nsSegments[i], + statements: outerNs, + ...finishNode(nsSegments[i].pos), + }; } return outerNs; @@ -359,13 +351,11 @@ export function parse(code: string | SourceFile) { const name = parseIdentifierOrMemberExpression(); parseExpected(Token.Semicolon); - return finishNode( - { - kind: SyntaxKind.UsingStatement, - name, - }, - pos - ); + return { + kind: SyntaxKind.UsingStatement, + name, + ...finishNode(pos), + }; } function parseOperationStatement(decorators: DecoratorExpressionNode[]): OperationStatementNode { @@ -379,28 +369,24 @@ export function parse(code: string | SourceFile) { const returnType = parseExpression(); parseExpected(Token.Semicolon); - return finishNode( - { - kind: SyntaxKind.OperationStatement, - id, - parameters, - returnType, - decorators, - }, - pos - ); + return { + kind: SyntaxKind.OperationStatement, + id, + parameters, + returnType, + decorators, + ...finishNode(pos), + }; } function parseOperationParameters(): ModelExpressionNode { const pos = tokenPos(); const properties = parseList(ListKind.OperationParameters, parseModelPropertyOrSpread); - const parameters: ModelExpressionNode = finishNode( - { - kind: SyntaxKind.ModelExpression, - properties, - }, - pos - ); + const parameters: ModelExpressionNode = { + kind: SyntaxKind.ModelExpression, + properties, + ...finishNode(pos), + }; return parameters; } @@ -420,32 +406,28 @@ export function parse(code: string | SourceFile) { const assignment = parseExpression(); parseExpected(Token.Semicolon); - return finishNode( - { - kind: SyntaxKind.ModelStatement, - id, - heritage: [], - templateParameters, - assignment, - decorators, - }, - pos - ); + return { + kind: SyntaxKind.ModelStatement, + id, + heritage: [], + templateParameters, + assignment, + decorators, + ...finishNode(pos), + }; } else { const heritage: ReferenceExpression[] = parseOptionalModelHeritage(); const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); - return finishNode( - { - kind: SyntaxKind.ModelStatement, - id, - heritage, - templateParameters, - decorators, - properties, - }, - pos - ); + return { + kind: SyntaxKind.ModelStatement, + id, + heritage, + templateParameters, + decorators, + properties, + ...finishNode(pos), + }; } } @@ -463,13 +445,11 @@ export function parse(code: string | SourceFile) { ): TemplateParameterDeclarationNode { reportInvalidDecorators(decorators, "template parameter"); const id = parseIdentifier(); - return finishNode( - { - kind: SyntaxKind.TemplateParameterDeclaration, - id, - }, - pos - ); + return { + kind: SyntaxKind.TemplateParameterDeclaration, + id, + ...finishNode(pos), + }; } function parseModelPropertyOrSpread(pos: number, decorators: DecoratorExpressionNode[]) { @@ -489,13 +469,11 @@ export function parse(code: string | SourceFile) { // This could be broadened to allow any type expression const target = parseReferenceExpression(); - return finishNode( - { - kind: SyntaxKind.ModelSpreadProperty, - target, - }, - pos - ); + return { + kind: SyntaxKind.ModelSpreadProperty, + target, + ...finishNode(pos), + }; } function parseModelProperty( @@ -511,16 +489,14 @@ export function parse(code: string | SourceFile) { parseExpected(Token.Colon); const value = parseExpression(); - return finishNode( - { - kind: SyntaxKind.ModelProperty, - id, - decorators, - value, - optional, - }, - pos - ); + return { + kind: SyntaxKind.ModelProperty, + id, + decorators, + value, + optional, + ...finishNode(pos), + }; } function parseExpression(): Expression { @@ -536,22 +512,17 @@ export function parse(code: string | SourceFile) { return node; } - node = finishNode( - { - kind: SyntaxKind.UnionExpression, - options: [node], - }, - pos - ); - + const options = [node]; while (parseOptional(Token.Bar)) { const expr = parseIntersectionExpressionOrHigher(); - node.options.push(expr); + options.push(expr); } - node.end = tokenPos(); - - return node; + return { + kind: SyntaxKind.UnionExpression, + options, + ...finishNode(pos), + }; } function parseIntersectionExpressionOrHigher(): Expression { @@ -563,22 +534,17 @@ export function parse(code: string | SourceFile) { return node; } - node = finishNode( - { - kind: SyntaxKind.IntersectionExpression, - options: [node], - }, - pos - ); - + const options = [node]; while (parseOptional(Token.Ampersand)) { const expr = parseArrayExpressionOrHigher(); - node.options.push(expr); + options.push(expr); } - node.end = tokenPos(); - - return node; + return { + kind: SyntaxKind.UnionExpression, + options, + ...finishNode(pos), + }; } function parseArrayExpressionOrHigher(): Expression { @@ -588,13 +554,11 @@ export function parse(code: string | SourceFile) { while (parseOptional(Token.OpenBracket)) { parseExpected(Token.CloseBracket); - expr = finishNode( - { - kind: SyntaxKind.ArrayExpression, - elementType: expr, - }, - pos - ); + expr = { + kind: SyntaxKind.ArrayExpression, + elementType: expr, + ...finishNode(pos), + }; } return expr; @@ -605,14 +569,12 @@ export function parse(code: string | SourceFile) { const target = parseIdentifierOrMemberExpression(); const args = parseOptionalList(ListKind.TemplateArguments, parseExpression); - return finishNode( - { - kind: SyntaxKind.TypeReference, - target, - arguments: args, - }, - pos - ); + return { + kind: SyntaxKind.TypeReference, + target, + arguments: args, + ...finishNode(pos), + }; } function parseImportStatement(): ImportStatementNode { @@ -622,13 +584,11 @@ export function parse(code: string | SourceFile) { const path = parseStringLiteral(); parseExpected(Token.Semicolon); - return finishNode( - { - kind: SyntaxKind.ImportStatement, - path, - }, - pos - ); + return { + kind: SyntaxKind.ImportStatement, + path, + ...finishNode(pos), + }; } function parseDecoratorExpression(): DecoratorExpressionNode { @@ -637,14 +597,12 @@ export function parse(code: string | SourceFile) { const target = parseIdentifierOrMemberExpression(); const args = parseOptionalList(ListKind.DecoratorArguments, parseExpression); - return finishNode( - { - kind: SyntaxKind.DecoratorExpression, - arguments: args, - target, - }, - pos - ); + return { + kind: SyntaxKind.DecoratorExpression, + arguments: args, + target, + ...finishNode(pos), + }; } function parseIdentifierOrMemberExpression(): IdentifierNode | MemberExpressionNode { @@ -652,14 +610,12 @@ export function parse(code: string | SourceFile) { while (parseOptional(Token.Dot)) { const pos = tokenPos(); - base = finishNode( - { - kind: SyntaxKind.MemberExpression, - base, - id: parseIdentifier(), - }, - pos - ); + base = { + kind: SyntaxKind.MemberExpression, + base, + id: parseIdentifier(), + ...finishNode(pos), + }; } return base; @@ -698,44 +654,38 @@ export function parse(code: string | SourceFile) { parseExpected(Token.OpenParen); const expr = parseExpression(); parseExpected(Token.CloseParen); - return finishNode(expr, pos); + return { ...expr, ...finishNode(pos) }; } function parseTupleExpression(): TupleExpressionNode { const pos = tokenPos(); const values = parseList(ListKind.Tuple, parseExpression); - return finishNode( - { - kind: SyntaxKind.TupleExpression, - values, - }, - pos - ); + return { + kind: SyntaxKind.TupleExpression, + values, + ...finishNode(pos), + }; } function parseModelExpression(): ModelExpressionNode { const pos = tokenPos(); const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); - return finishNode( - { - kind: SyntaxKind.ModelExpression, - properties, - }, - pos - ); + return { + kind: SyntaxKind.ModelExpression, + properties, + ...finishNode(pos), + }; } function parseStringLiteral(): StringLiteralNode { const pos = tokenPos(); const value = tokenValue(); parseExpected(Token.StringLiteral); - return finishNode( - { - kind: SyntaxKind.StringLiteral, - value, - }, - pos - ); + return { + kind: SyntaxKind.StringLiteral, + value, + ...finishNode(pos), + }; } function parseNumericLiteral(): NumericLiteralNode { @@ -744,27 +694,22 @@ export function parse(code: string | SourceFile) { const value = Number(text); parseExpected(Token.NumericLiteral); - return finishNode( - { - kind: SyntaxKind.NumericLiteral, - text, - value, - }, - pos - ); + return { + kind: SyntaxKind.NumericLiteral, + value, + ...finishNode(pos), + }; } function parseBooleanLiteral(): BooleanLiteralNode { const pos = tokenPos(); const token = parseExpectedOneOf(Token.TrueKeyword, Token.FalseKeyword); const value = token === Token.TrueKeyword; - return finishNode( - { - kind: SyntaxKind.BooleanLiteral, - value, - }, - pos - ); + return { + kind: SyntaxKind.BooleanLiteral, + value, + ...finishNode(pos), + }; } function parseIdentifier(message?: string): IdentifierNode { @@ -781,13 +726,11 @@ export function parse(code: string | SourceFile) { const sv = tokenValue(); nextToken(); - return finishNode( - { - kind: SyntaxKind.Identifier, - sv, - }, - pos - ); + return { + kind: SyntaxKind.Identifier, + sv, + ...finishNode(pos), + }; } // utility functions @@ -816,21 +759,18 @@ export function parse(code: string | SourceFile) { function createMissingIdentifier(): IdentifierNode { missingIdentifierCounter++; - return finishNode( - { - kind: SyntaxKind.Identifier, - sv: "" + missingIdentifierCounter, - }, - tokenPos() - ); + return { + kind: SyntaxKind.Identifier, + sv: "" + missingIdentifierCounter, + ...finishNode(tokenPos()), + }; } - function finishNode(o: T, pos: number): T & TextRange & { flags: NodeFlags } { + function finishNode(pos: number): TextRange & { flags: NodeFlags } { const flags = parseErrorInNextFinishedNode ? NodeFlags.ThisNodeHasError : NodeFlags.None; parseErrorInNextFinishedNode = false; return { - ...o, pos, end: previousTokenEnd, flags, @@ -953,7 +893,7 @@ export function parse(code: string | SourceFile) { function parseEmptyStatement(): EmptyStatementNode { const pos = tokenPos(); parseExpected(Token.Semicolon); - return finishNode({ kind: SyntaxKind.EmptyStatement }, pos); + return { kind: SyntaxKind.EmptyStatement, ...finishNode(pos) }; } function parseInvalidStatement(): InvalidStatementNode { @@ -972,7 +912,7 @@ export function parse(code: string | SourceFile) { ); error("Statement expected.", { pos, end: previousTokenEnd }); - return finishNode({ kind: SyntaxKind.InvalidStatement }, pos); + return { kind: SyntaxKind.InvalidStatement, ...finishNode(pos) }; } function error(message: string, target?: TextRange & { realPos?: number }) { From 9bda6a8fc990567de39a4edffa5cf1cfcd277f70 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Mon, 26 Apr 2021 14:33:08 -0700 Subject: [PATCH 11/47] Inform tsc of project-to-project dependencies (#495) Fixes issues with watch build when a change impacts dependent projects. Also fix issues preventing a watch build in VS Code from being successful due to dist/ or dist-dev/ not being created yet. With both of these, it is now actually possible to do only rush update on pristine enlistment, then Ctrl+Shift+B in VS Code as CONTRIBUTING.md claims. --- eng/scripts/helpers.js | 61 +++++++++++++++++++-------- packages/adl-rest/tsconfig.json | 1 + packages/adl-vscode/package.json | 2 + packages/adl-vscode/src/tmlanguage.ts | 2 + packages/adl-vscode/tsconfig.json | 1 + 5 files changed, 50 insertions(+), 17 deletions(-) diff --git a/eng/scripts/helpers.js b/eng/scripts/helpers.js index d261cabd4..479a5e0e3 100644 --- a/eng/scripts/helpers.js +++ b/eng/scripts/helpers.js @@ -1,5 +1,5 @@ import { spawn, spawnSync } from "child_process"; -import { readFileSync } from "fs"; +import { statSync, readFileSync } from "fs"; import { dirname, resolve } from "path"; import { fileURLToPath } from "url"; @@ -108,24 +108,51 @@ export function runWatch(watch, dir, build, options) { let lastStartTime; dir = resolve(dir); - // build once up-front. - runBuild(); + // We might need to wait for another watcher to create the directory. + try { + statSync(dir); + } catch (err) { + if (err.code === "ENOENT") { + waitForDirectoryCreation(); + return; + } + throw err; + } - watch.createMonitor(dir, { interval: 0.2, ...options }, (monitor) => { - let handler = function (file) { - if (lastStartTime && monitor?.files[file]?.mtime < lastStartTime) { - // File was changed before last build started so we can ignore it. This - // avoids running the build unnecessarily when a series of input files - // change at the same time. - return; - } - runBuild(file); - }; + start(); - monitor.on("created", handler); - monitor.on("changed", handler); - monitor.on("removed", handler); - }); + function waitForDirectoryCreation() { + logWithTime(`${dir} doesn't exist yet: waiting for it to be created.`); + watch.createMonitor(dirname(dir), "created", (monitor) => { + monitor.on("created", (file) => { + if (file === dir) { + logWithTime(`${dir} created.`); + start(); + } + }); + }); + } + + function start() { + // build once up-front. + runBuild(); + + watch.createMonitor(dir, { interval: 0.2, ...options }, (monitor) => { + let handler = function (file) { + if (lastStartTime && monitor?.files[file]?.mtime < lastStartTime) { + // File was changed before last build started so we can ignore it. This + // avoids running the build unnecessarily when a series of input files + // change at the same time. + return; + } + runBuild(file); + }; + + monitor.on("created", handler); + monitor.on("changed", handler); + monitor.on("removed", handler); + }); + } function runBuild(file) { runBuildAsync(file).catch((err) => { diff --git a/packages/adl-rest/tsconfig.json b/packages/adl-rest/tsconfig.json index 84c460a80..0e8075fa9 100644 --- a/packages/adl-rest/tsconfig.json +++ b/packages/adl-rest/tsconfig.json @@ -1,5 +1,6 @@ { "extends": "../tsconfig.json", + "references": [{ "path": "../adl/tsconfig.json" }], "compilerOptions": { "outDir": "dist", "rootDir": "src" diff --git a/packages/adl-vscode/package.json b/packages/adl-vscode/package.json index 4868b1af0..5dd934a48 100644 --- a/packages/adl-vscode/package.json +++ b/packages/adl-vscode/package.json @@ -84,8 +84,10 @@ "@azure-tools/tmlanguage-generator": "0.1.4", "@rollup/plugin-commonjs": "~17.1.0", "@rollup/plugin-node-resolve": "~11.2.0", + "@types/mkdirp": "~1.0.1", "@types/node": "~14.0.27", "@types/vscode": "~1.53.0", + "mkdirp": "~1.0.4", "rollup": "~2.41.4", "typescript": "~4.2.4", "vsce": "~1.85.1", diff --git a/packages/adl-vscode/src/tmlanguage.ts b/packages/adl-vscode/src/tmlanguage.ts index fba12ebab..153ac94ab 100644 --- a/packages/adl-vscode/src/tmlanguage.ts +++ b/packages/adl-vscode/src/tmlanguage.ts @@ -3,6 +3,7 @@ import * as tm from "@azure-tools/tmlanguage-generator"; import fs from "fs/promises"; +import mkdirp from "mkdirp"; import { resolve } from "path"; type IncludeRule = tm.IncludeRule; @@ -333,5 +334,6 @@ export async function main() { const plist = await tm.emitPList(grammar, { errorSourceFilePath: resolve("./src/tmlanguage.ts"), }); + await mkdirp("./dist"); await fs.writeFile("./dist/adl.tmLanguage", plist); } diff --git a/packages/adl-vscode/tsconfig.json b/packages/adl-vscode/tsconfig.json index f98b80b3c..f8b95ab10 100644 --- a/packages/adl-vscode/tsconfig.json +++ b/packages/adl-vscode/tsconfig.json @@ -1,5 +1,6 @@ { "extends": "../tsconfig.json", + "references": [{ "path": "../tmlanguage-generator/tsconfig.json" }], "compilerOptions": { "outDir": "dist-dev", "rootDir": "src", From 1da60d073080b06fc63a88b8670b576d7845ee86 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Tue, 27 Apr 2021 14:28:50 -0700 Subject: [PATCH 12/47] Fix some edge cases in watch build helper (#502) * Stop monitoring for dir to be created once it has been * Avoid running initial build twice if dir hasn't been created yet * Don't rely on mtime for created or removed events * Don't clear screen on initial build so we see startup logging --- eng/scripts/helpers.js | 61 +++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/eng/scripts/helpers.js b/eng/scripts/helpers.js index 479a5e0e3..46a23f294 100644 --- a/eng/scripts/helpers.js +++ b/eng/scripts/helpers.js @@ -105,10 +105,15 @@ export function clearScreen() { } export function runWatch(watch, dir, build, options) { - let lastStartTime; + let lastBuildTime; dir = resolve(dir); - // We might need to wait for another watcher to create the directory. + // We need to wait for directory to be created before watching it. This deals + // with races between watchers where one watcher must create a directory + // before another can watch it. + // + // For example, we can't watch for tmlanguage.js changes if the source watcher + // hasn't even created the directory in which tmlanguage.js will be written. try { statSync(dir); } catch (err) { @@ -119,14 +124,19 @@ export function runWatch(watch, dir, build, options) { throw err; } + // Directory already exists: we can start watching right away. start(); function waitForDirectoryCreation() { - logWithTime(`${dir} doesn't exist yet: waiting for it to be created.`); - watch.createMonitor(dirname(dir), "created", (monitor) => { + let dirCreated = false; + let parentDir = dirname(dir); + logWithTime(`Waiting for ${dir} to be created.`); + + watch.createMonitor(parentDir, "created", (monitor) => { monitor.on("created", (file) => { - if (file === dir) { - logWithTime(`${dir} created.`); + if (!dirCreated && file === dir) { + dirCreated = true; // defend against duplicate events. + monitor.stop(); start(); } }); @@ -134,39 +144,36 @@ export function runWatch(watch, dir, build, options) { } function start() { - // build once up-front. + // build once up-front runBuild(); + // then build again on any change watch.createMonitor(dir, { interval: 0.2, ...options }, (monitor) => { - let handler = function (file) { - if (lastStartTime && monitor?.files[file]?.mtime < lastStartTime) { - // File was changed before last build started so we can ignore it. This - // avoids running the build unnecessarily when a series of input files - // change at the same time. - return; - } - runBuild(file); - }; - - monitor.on("created", handler); - monitor.on("changed", handler); - monitor.on("removed", handler); + monitor.on("created", (file) => runBuild(`${file} created`)); + monitor.on("removed", (file) => runBuild(`${file} removed`)); + monitor.on("changed", (file) => runBuild(`${file} changed`, monitor.files[file]?.mtime)); }); } - function runBuild(file) { - runBuildAsync(file).catch((err) => { + function runBuild(changeDescription, changeTime) { + runBuildAsync(changeDescription, changeTime).catch((err) => { console.error(err.stack); process.exit(1); }); } - async function runBuildAsync(file) { - lastStartTime = Date.now(); - clearScreen(); + async function runBuildAsync(changeDescription, changeTime) { + if (changeTime && lastBuildTime && changeTime < lastBuildTime) { + // Don't rebuild if a change happened before the last build kicked off. + // Defends against duplicate events and building more than once when a + // bunch of files are changed at the same time. + return; + } - if (file) { - logWithTime(`File change detected: ${file}. Running build.`); + lastBuildTime = new Date(); + if (changeDescription) { + clearScreen(); + logWithTime(`File change detected: ${changeDescription}. Running build.`); } else { logWithTime("Starting build in watch mode."); } From b93b561fc4d43e55f64d588a55133b4e78943560 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Wed, 28 Apr 2021 08:02:54 -0700 Subject: [PATCH 13/47] Allow leading +/- in numeric literals and require fractional digits (#503) --- packages/adl-language/src/spec.emu.html | 9 ++- packages/adl/compiler/scanner.ts | 54 +++++++----------- packages/adl/test/test-parser.ts | 74 ++++++++++++++++++++++--- packages/adl/test/test-scanner.ts | 7 +-- 4 files changed, 92 insertions(+), 52 deletions(-) diff --git a/packages/adl-language/src/spec.emu.html b/packages/adl-language/src/spec.emu.html index 02cf62296..8dbe8ade2 100644 --- a/packages/adl-language/src/spec.emu.html +++ b/packages/adl-language/src/spec.emu.html @@ -65,13 +65,16 @@ BooleanLiteral : NumericLiteral : DecimalLiteral HexIntegerLiteral + BinaryIntegerLiteral DecimalLiteral : - DecimalIntegerLiteral `.` DecimalDigits? ExponentPart? + DecimalIntegerLiteral `.` DecimalDigits ExponentPart? DecimalIntegerLiteral ExponentPart? DecimalIntegerLiteral : DecimalDigits + `+` DecimalDigits + `-` DecimalDigits DecimalDigits : DecimalDigit @@ -81,9 +84,9 @@ DecimalDigit : one of `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` ExponentPart : - `e` SignedInteger + `e` DecimalIntegerLiteral -SignedInteger : +DecimalIntegerInteger : DecimalDigits `+` DecimalDigits `-` DecimalDigits diff --git a/packages/adl/compiler/scanner.ts b/packages/adl/compiler/scanner.ts index 73db4a65e..58243b3e9 100644 --- a/packages/adl/compiler/scanner.ts +++ b/packages/adl/compiler/scanner.ts @@ -332,6 +332,10 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro } return scanInvalidCharacter(); + case CharCode.Plus: + case CharCode.Minus: + return isDigit(lookAhead(1)) ? scanSignedNumber() : scanInvalidCharacter(); + case CharCode._0: switch (lookAhead(1)) { case CharCode.x: @@ -447,17 +451,24 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro return (token = Token.Whitespace); } + function scanSignedNumber() { + position++; // consume '+/-' + return scanNumber(); + } + function scanNumber() { scanKnownDigits(); - if (!eof()) { - switch (input.charCodeAt(position)) { - case CharCode.Dot: - scanFractionAndExponent(); - break; - case CharCode.e: - scanExponent(); - break; + if (!eof() && input.charCodeAt(position) === CharCode.Dot) { + position++; + scanRequiredDigits(); + } + if (!eof() && input.charCodeAt(position) === CharCode.e) { + position++; + const ch = input.charCodeAt(position); + if (ch === CharCode.Plus || ch === CharCode.Minus) { + position++; } + scanRequiredDigits(); } return (token = Token.NumericLiteral); } @@ -468,12 +479,6 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro } while (!eof() && isDigit(input.charCodeAt(position))); } - function scanOptionalDigits() { - if (!eof() && isDigit(input.charCodeAt(position))) { - scanKnownDigits(); - } - } - function scanRequiredDigits() { if (eof() || !isDigit(input.charCodeAt(position))) { error(Message.DigitExpected); @@ -482,27 +487,6 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro scanKnownDigits(); } - function scanFractionAndExponent() { - position++; // consume '.' - scanOptionalDigits(); - if (!eof() && input.charCodeAt(position) === CharCode.e) { - scanExponent(); - } - } - - function scanExponent() { - position++; // consume 'e' - if (eof()) { - error(Message.DigitExpected); - return; - } - const ch = input.charCodeAt(position); - if (ch === CharCode.Plus || ch === CharCode.Minus) { - position++; - } - scanRequiredDigits(); - } - function scanHexNumber() { position += 2; // consume '0x' diff --git a/packages/adl/test/test-parser.ts b/packages/adl/test/test-parser.ts index 617fd6e22..f64ae864b 100644 --- a/packages/adl/test/test-parser.ts +++ b/packages/adl/test/test-parser.ts @@ -226,11 +226,6 @@ describe("syntax", () => { ['model X = """\nbanana', [/Unterminated string literal/]], ['model X = """\nbanana\\', [/Unterminated string literal/]], ["/* Yada yada yada", [/Unterminated comment/]], - ["123.0e", [/Digit expected/]], - ["123.e", [/Digit expected/]], - ["123e", [/Digit expected/]], - ["0b", [/Binary digit expected/]], - ["0x", [/Hexadecimal digit expected/]], ]); }); @@ -239,7 +234,6 @@ describe("syntax", () => { ["model X = 0x10101", [/';' expected/]], ["model X = 0xBEEF", [/';' expected/]], ["model X = 123", [/';' expected/]], - ["model X = 123.", [/';' expected/]], ["model X = 123e45", [/';' expected/]], ["model X = 123.45", [/';' expected/]], ["model X = 123.45e2", [/';' expected/]], @@ -249,6 +243,65 @@ describe("syntax", () => { ]); }); + describe("numeric literals", () => { + const good: [string, number][] = [ + // Some questions remain here: https://github.com/Azure/adl/issues/506 + ["-0", -0], + ["1e9999", Infinity], + ["1e-9999", 0], + ["-1e-9999", -0], + ["-1e9999", -Infinity], + + // NOTE: No octal in ADL + ["077", 77], + ["+077", 77], + ["-077", -77], + + ["0xABCD", 0xabcd], + ["0xabcd", 0xabcd], + ["0x1010", 0x1010], + ["0b1010", 0b1010], + ["0", 0], + ["+0", 0], + ["0.0", 0.0], + ["+0.0", 0], + ["-0.0", -0.0], + ["123", 123], + ["+123", 123], + ["-123", -123], + ["123.123", 123.123], + ["+123.123", 123.123], + ["-123.123", -123.123], + ["789e42", 789e42], + ["+789e42", 789e42], + ["-789e42", -789e42], + ["654.321e9", 654.321e9], + ["+654.321e9", 654.321e9], + ["-654.321e9", -654.321e9], + ]; + + const bad: [string, RegExp][] = [ + ["123.", /Digit expected/], + ["123.0e", /Digit expected/], + ["123e", /Digit expected/], + ["0b", /Binary digit expected/], + ["0b2", /Binary digit expected/], + ["0x", /Hexadecimal digit expected/], + ["0xG", /Hexadecimal digit expected/], + ]; + + parseEach(good.map((c) => [`model M = ${c[0]};`, (node) => isNumericLiteral(node, c[1])])); + parseErrorEach(bad.map((c) => [`model M = ${c[0]};`, [c[1]]])); + + function isNumericLiteral(node: ADLScriptNode, value: number) { + const statement = node.statements[0]; + assert(statement.kind === SyntaxKind.ModelStatement, "model statement expected"); + const assignment = statement.assignment; + assert(assignment?.kind === SyntaxKind.NumericLiteral, "numeric literal expected"); + assert.strictEqual(assignment.value, value); + } + }); + describe("non-ascii identifiers", () => { parseEach([ "model Incompréhensible {}", @@ -261,14 +314,19 @@ describe("syntax", () => { }); }); -function parseEach(cases: string[]) { - for (const code of cases) { +function parseEach(cases: (string | [string, (node: ADLScriptNode) => void])[]) { + for (const each of cases) { + const code = typeof each === "string" ? each : each[0]; + const callback = typeof each === "string" ? undefined : each[1]; it("parses `" + shorten(code) + "`", () => { logVerboseTestOutput("=== Source ==="); logVerboseTestOutput(code); logVerboseTestOutput("\n=== Parse Result ==="); const astNode = parse(code); + if (callback) { + callback(astNode); + } dumpAST(astNode); logVerboseTestOutput("\n=== Diagnostics ==="); diff --git a/packages/adl/test/test-scanner.ts b/packages/adl/test/test-scanner.ts index 5378328e4..07404ca94 100644 --- a/packages/adl/test/test-scanner.ts +++ b/packages/adl/test/test-scanner.ts @@ -159,7 +159,7 @@ describe("scanner", () => { }); it("scans numeric literals", () => { - const all = tokens("42 0xBEEF 0b1010 1.5e4 314.0e-2 1e+1000 3. 2.e3"); + const all = tokens("42 0xBEEF 0b1010 1.5e4 314.0e-2 1e+1000"); verify(all, [ [Token.NumericLiteral, "42"], [Token.Whitespace], @@ -172,11 +172,6 @@ describe("scanner", () => { [Token.NumericLiteral, "314.0e-2"], [Token.Whitespace], [Token.NumericLiteral, "1e+1000"], - [Token.Whitespace], - // https://github.com/Azure/adl/issues/488 - we may want to disallow these - [Token.NumericLiteral, "3."], - [Token.Whitespace], - [Token.NumericLiteral, "2.e3"], ]); }); From 051862aa54379efdb8e5246a57f033692697bb17 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Wed, 28 Apr 2021 13:31:31 -0700 Subject: [PATCH 14/47] Fix some end-to-end client generation issues (#507) 1. `compile` had lost its success message. I inadvertently deleted it a while ago: restored. 2. `generate` was printing undefined as the output path: fixed. 3. `generate` did nothing without --client, required --client for now. 4. `generate` took a long time to do nothing without --language, required --language. 5. `generate` a client inside the adl tree, and tsc would to compile it: excluded 'adl-output' in tsconfig. Also routed running of autorest through run helper so you can see the autorest args we pass using `adl --debug` --- packages/adl/compiler/cli.ts | 100 +++++++++++++++++++++-------------- packages/adl/tsconfig.json | 2 +- 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/packages/adl/compiler/cli.ts b/packages/adl/compiler/cli.ts index 3d5816f89..f6211516f 100644 --- a/packages/adl/compiler/cli.ts +++ b/packages/adl/compiler/cli.ts @@ -1,4 +1,4 @@ -import { spawnSync } from "child_process"; +import { spawnSync, SpawnSyncOptionsWithBufferEncoding } from "child_process"; import { mkdtemp, readdir, rmdir } from "fs/promises"; import mkdirp from "mkdirp"; import os from "os"; @@ -42,32 +42,38 @@ const args = yargs(process.argv.slice(2)) "generate ", "Generate client and server code from a directory of ADL files.", (cmd) => { - return cmd - .positional("path", { - description: "The path to folder containing .adl files", - type: "string", - }) - .option("client", { - type: "boolean", - describe: "Generate a client library for the ADL definition", - }) - .option("language", { - type: "string", - choices: ["typescript", "csharp", "python"], - describe: "The language to use for code generation", - }) - .option("output-path", { - type: "string", - default: "./adl-output", - describe: - "The output path for generated artifacts. If it does not exist, it will be created.", - }) - .option("option", { - type: "array", - string: true, - describe: - "Key/value pairs that can be passed to ADL components. The format is 'key=value'. This parameter can be used multiple times to add more options.", - }); + return ( + cmd + .positional("path", { + description: "The path to folder containing .adl files", + type: "string", + }) + .option("client", { + type: "boolean", + describe: "Generate a client library for the ADL definition", + }) + .option("language", { + type: "string", + choices: ["typescript", "csharp", "python"], + describe: "The language to use for code generation", + }) + .option("output-path", { + type: "string", + default: "./adl-output", + describe: + "The output path for generated artifacts. If it does not exist, it will be created.", + }) + .option("option", { + type: "array", + string: true, + describe: + "Key/value pairs that can be passed to ADL components. The format is 'key=value'. This parameter can be used multiple times to add more options.", + }) + // we can't generate anything but a client yet + .demandOption("client") + // and language is required to do so + .demandOption("language") + ); } ) .command("code", "Manage VS Code Extension.", (cmd) => { @@ -90,9 +96,14 @@ const args = yargs(process.argv.slice(2)) .version(adlVersion) .demandCommand(1, "You must use one of the supported commands.").argv; -async function compileInput(compilerOptions: CompilerOptions) { +async function compileInput(compilerOptions: CompilerOptions, printSuccess = true) { try { await compile(args.path!, NodeHost, compilerOptions); + if (printSuccess) { + console.log( + `Compilation completed successfully, output files are in ${compilerOptions.outputPath}.` + ); + } } catch (err) { if (err instanceof DiagnosticError) { logDiagnostics(err.diagnostics, console.error); @@ -123,6 +134,7 @@ async function getCompilerOptions(): Promise { return { miscOptions, + outputPath, swaggerOutputFile: resolve(args["output-path"], "openapi.json"), nostdlib: args["nostdlib"], }; @@ -134,7 +146,8 @@ async function generateClient(options: CompilerOptions) { const autoRestPath = new url.URL(`../../node_modules/.bin/${autoRestBin}`, import.meta.url); // Execute AutoRest on the output file - const result = spawnSync( + console.log(); //newline between compilation output and generation output + const result = run( url.fileURLToPath(autoRestPath), [ `--${args.language}`, @@ -144,15 +157,14 @@ async function generateClient(options: CompilerOptions) { `--input-file=${options.swaggerOutputFile}`, ], { - stdio: "inherit", shell: true, } ); if (result.status === 0) { - console.log(`Generation completed successfully, output files are in ${options.outputPath}.`); + console.log(`\nGeneration completed successfully, output files are in ${clientPath}.`); } else { - console.error("\nAn error occurred during client generation."); + console.error("\nClient generation failed."); process.exit(result.status || 1); } } @@ -182,14 +194,21 @@ async function installVsix(pkg: string, install: (vsixPath: string) => void) { await rmdir(temp, { recursive: true }); } +async function runCode(codeArgs: string[]) { + await run(args.insiders ? "code-insiders" : "code", codeArgs, { + // VS Code's CLI emits node warnings that we can't do anything about. Suppress them. + env: { ...process.env, NODE_NO_WARNINGS: "1" }, + }); +} + async function installVSCodeExtension() { await installVsix("adl-vscode", (vsix) => { - run(args.insiders ? "code-insiders" : "code", ["--install-extension", vsix]); + runCode(["--install-extension", vsix]); }); } async function uninstallVSCodeExtension() { - run(args.insiders ? "code-insiders" : "code", ["--uninstall-extension", "microsoft.adl-vscode"]); + await runCode(["--uninstall-extension", "microsoft.adl-vscode"]); } function getVsixInstallerPath(): string { @@ -221,9 +240,9 @@ async function uninstallVSExtension() { // ENOENT checking and handles spaces poorly in some cases. const isCmdOnWindows = ["code", "code-insiders", "npm"]; -function run(command: string, commandArgs: string[]) { +function run(command: string, commandArgs: string[], options?: SpawnSyncOptionsWithBufferEncoding) { if (args.debug) { - console.log(`> ${command} ${commandArgs.join(" ")}`); + console.log(`> ${command} ${commandArgs.join(" ")}\n`); } const baseCommandName = path.basename(command); @@ -233,8 +252,7 @@ function run(command: string, commandArgs: string[]) { const proc = spawnSync(command, commandArgs, { stdio: "inherit", - // VS Code's CLI emits node warnings that we can't do anything about. Suppress them. - env: { ...process.env, NODE_NO_WARNINGS: "1" }, + ...(options ?? {}), }); if (proc.error) { @@ -255,8 +273,10 @@ function run(command: string, commandArgs: string[]) { proc.status }.` ); - process.exit(proc.status ?? 1); + process.exit(proc.status || 1); } + + return proc; } async function main() { @@ -272,7 +292,7 @@ async function main() { break; case "generate": options = await getCompilerOptions(); - await compileInput(options); + await compileInput(options, false); if (args.client) { await generateClient(options); } diff --git a/packages/adl/tsconfig.json b/packages/adl/tsconfig.json index bf9521076..32ff9167e 100644 --- a/packages/adl/tsconfig.json +++ b/packages/adl/tsconfig.json @@ -6,5 +6,5 @@ "types": ["node", "mocha"] }, "include": ["./**/*.ts"], - "exclude": ["dist", "node_modules", "temp"] + "exclude": ["dist", "node_modules", "temp", "adl-output"] } From dcb8f16cbde740a2c118d3e2e0270a411fb552a4 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 29 Apr 2021 14:10:39 -0700 Subject: [PATCH 15/47] Guard against fuzz tests being run accidentally --- packages/adl/package.json | 2 +- packages/adl/test/manual/fuzz.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/adl/package.json b/packages/adl/package.json index f03fd7a97..85d3e5db8 100644 --- a/packages/adl/package.json +++ b/packages/adl/package.json @@ -40,7 +40,7 @@ "test": "mocha --timeout 5000 --require source-map-support/register --ignore 'dist/test/manual/**/*.js' 'dist/test/**/*.js'", "regen-samples": "node scripts/regen-samples.js", "regen-nonascii": "node scripts/regen-nonascii.js", - "fuzz": "node dist/test/manual/fuzz.js" + "fuzz": "node dist/test/manual/fuzz.js run" }, "dependencies": { "autorest": "~3.0.6335", diff --git a/packages/adl/test/manual/fuzz.ts b/packages/adl/test/manual/fuzz.ts index 945f3f7c3..45a31839d 100644 --- a/packages/adl/test/manual/fuzz.ts +++ b/packages/adl/test/manual/fuzz.ts @@ -43,6 +43,11 @@ const weights = { main(); function main() { + if (process.argv[2] !== "run") { + throw new Error( + "Correct usage is `node fuzz.js run`. Is there a missing/incorrect mocha exclude pattern causing this to load?" + ); + } const iterations = 10000; console.log("Running parser fuzz test with 1000 iterations..."); fuzzTest(iterations); From 8c86e044278d1dcf6076795ec54661f580c5145a Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Mon, 3 May 2021 11:14:24 -0700 Subject: [PATCH 16/47] Implement alias and enum, remove model = (#504) --- packages/adl-language/src/spec.emu.html | 48 ++++-- packages/adl-rest/src/rest.ts | 124 ++++++++------ packages/adl-vscode/src/tmlanguage.ts | 26 ++- packages/adl/compiler/binder.ts | 19 +++ packages/adl/compiler/checker.ts | 137 ++++++++++----- packages/adl/compiler/parser.ts | 138 ++++++++++++---- packages/adl/compiler/program.ts | 28 +++- packages/adl/compiler/scanner.ts | 19 ++- packages/adl/compiler/types.ts | 59 ++++++- packages/adl/lib/decorators.ts | 140 ++++++++-------- packages/adl/test/checker/alias.ts | 165 +++++++++++++++++++ packages/adl/test/checker/enum.ts | 76 +++++++++ packages/adl/test/checker/namespaces.ts | 2 +- packages/adl/test/checker/using.ts | 2 +- packages/adl/test/decorators/range-limits.ts | 8 +- packages/adl/test/libraries/simple/main.adl | 4 +- packages/adl/test/test-host.ts | 62 +++++-- packages/adl/test/test-parser.ts | 74 ++++++--- packages/adl/test/test-scanner.ts | 29 +++- 19 files changed, 900 insertions(+), 260 deletions(-) create mode 100644 packages/adl/test/checker/alias.ts create mode 100644 packages/adl/test/checker/enum.ts diff --git a/packages/adl-language/src/spec.emu.html b/packages/adl-language/src/spec.emu.html index 8dbe8ade2..2d8729a90 100644 --- a/packages/adl-language/src/spec.emu.html +++ b/packages/adl-language/src/spec.emu.html @@ -206,6 +206,8 @@ Statement : NamespaceStatement OperationStatement UsingStatement + EnumStatement + AliasStatement `;` UsingStatement : @@ -213,22 +215,10 @@ UsingStatement : ModelStatement : DecoratorList? `model` Identifier TemplateParameters? ModelHeritage? `{` ModelBody? `}` - DecoratorList? `model` Identifier TemplateParameters? `=` Expression `;` ModelHeritage : `extends` ReferenceExpressionList -ReferenceExpressionList : - ReferenceExpression - ReferenceExpressionList `,` ReferenceExpression - -TemplateParameters : - `<` IdentifierList `>` - -IdentifierList : - Identifier - IdentifierList `,` Identifier - ModelBody : ModelPropertyList `,`? ModelPropertyList `;`? @@ -246,6 +236,40 @@ ModelProperty: ModelSpreadProperty : `...` ReferenceExpression +EnumStatement : + DecoratorList? `enum` Identifier `{` EnumBody? `}` + +EnumBody : + EnumMemberList `,`? + EnumMemberList `;`? + +EnumMemberList : + EnumMember + EnumMemberList `,` EnumMember + EnumMemberList `;` EnumMember + +EnumMember : + DecoratorList? Identifier EnumMemberValue? + DecoratorList? StringLiteral EnumMemberValue? + +EnumMemberValue : + `:` StringLiteral + `:` NumericLiteral + +AliasStatement : + `alias` Identifier TemplateParameters? `=` Expression; + +ReferenceExpressionList : + ReferenceExpression + ReferenceExpressionList `,` ReferenceExpression + +TemplateParameters : + `<` IdentifierList `>` + +IdentifierList : + Identifier + IdentifierList `,` Identifier + NamespaceStatement: DecoratorList? `namespace` IdentifierOrMemberExpression `{` StatementList? `}` diff --git a/packages/adl-rest/src/rest.ts b/packages/adl-rest/src/rest.ts index 2bd5d0b3a..30e91896c 100644 --- a/packages/adl-rest/src/rest.ts +++ b/packages/adl-rest/src/rest.ts @@ -1,67 +1,66 @@ import { NamespaceType, Program, throwDiagnostic, Type } from "@azure-tools/adl"; -const basePaths = new Map(); - +const basePathsKey = Symbol(); export function resource(program: Program, entity: Type, basePath = "") { if (entity.kind !== "Namespace") return; - basePaths.set(entity, basePath); + program.stateMap(basePathsKey).set(entity, basePath); } -export function getResources() { - return Array.from(basePaths.keys()); +export function getResources(program: Program) { + return Array.from(program.stateMap(basePathsKey).keys()); } -export function isResource(obj: Type) { - return basePaths.has(obj); +export function isResource(program: Program, obj: Type) { + return program.stateMap(basePathsKey).has(obj); } -export function basePathForResource(resource: Type) { - return basePaths.get(resource); +export function basePathForResource(program: Program, resource: Type) { + return program.stateMap(basePathsKey).get(resource); } -const headerFields = new Map(); +const headerFieldsKey = Symbol(); export function header(program: Program, entity: Type, headerName: string) { if (!headerName && entity.kind === "ModelProperty") { headerName = entity.name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); } - headerFields.set(entity, headerName); + program.stateMap(headerFieldsKey).set(entity, headerName); } -export function getHeaderFieldName(entity: Type) { - return headerFields.get(entity); +export function getHeaderFieldName(program: Program, entity: Type) { + return program.stateMap(headerFieldsKey).get(entity); } -const queryFields = new Map(); +const queryFieldsKey = Symbol(); export function query(program: Program, entity: Type, queryKey: string) { if (!queryKey && entity.kind === "ModelProperty") { queryKey = entity.name; } - queryFields.set(entity, queryKey); + program.stateMap(queryFieldsKey).set(entity, queryKey); } -export function getQueryParamName(entity: Type) { - return queryFields.get(entity); +export function getQueryParamName(program: Program, entity: Type) { + return program.stateMap(queryFieldsKey).get(entity); } -const pathFields = new Map(); +const pathFieldsKey = Symbol(); export function path(program: Program, entity: Type, paramName: string) { if (!paramName && entity.kind === "ModelProperty") { paramName = entity.name; } - pathFields.set(entity, paramName); + program.stateMap(pathFieldsKey).set(entity, paramName); } -export function getPathParamName(entity: Type) { - return pathFields.get(entity); +export function getPathParamName(program: Program, entity: Type) { + return program.stateMap(pathFieldsKey).get(entity); } -const bodyFields = new Set(); +const bodyFieldsKey = Symbol(); export function body(program: Program, entity: Type) { - bodyFields.add(entity); + program.stateSet(bodyFieldsKey).add(entity); } -export function isBody(entity: Type) { - return bodyFields.has(entity); +export function isBody(program: Program, entity: Type) { + return program.stateSet(bodyFieldsKey).has(entity); } export type HttpVerb = "get" | "put" | "post" | "patch" | "delete"; @@ -71,12 +70,12 @@ interface OperationRoute { subPath?: string; } -const operationRoutes = new Map(); +const operationRoutesKey = Symbol(); -function setOperationRoute(entity: Type, verb: OperationRoute) { +function setOperationRoute(program: Program, entity: Type, verb: OperationRoute) { if (entity.kind === "Operation") { - if (!operationRoutes.has(entity)) { - operationRoutes.set(entity, verb); + if (!program.stateMap(operationRoutesKey).has(entity)) { + program.stateMap(operationRoutesKey).set(entity, verb); } else { throwDiagnostic(`HTTP verb already applied to ${entity.name}`, entity); } @@ -85,33 +84,33 @@ function setOperationRoute(entity: Type, verb: OperationRoute) { } } -export function getOperationRoute(entity: Type): OperationRoute | undefined { - return operationRoutes.get(entity); +export function getOperationRoute(program: Program, entity: Type): OperationRoute | undefined { + return program.stateMap(operationRoutesKey).get(entity); } export function get(program: Program, entity: Type, subPath?: string) { - setOperationRoute(entity, { + setOperationRoute(program, entity, { verb: "get", subPath, }); } export function put(program: Program, entity: Type, subPath?: string) { - setOperationRoute(entity, { + setOperationRoute(program, entity, { verb: "put", subPath, }); } export function post(program: Program, entity: Type, subPath?: string) { - setOperationRoute(entity, { + setOperationRoute(program, entity, { verb: "post", subPath, }); } export function patch(program: Program, entity: Type, subPath?: string) { - setOperationRoute(entity, { + setOperationRoute(program, entity, { verb: "patch", subPath, }); @@ -119,7 +118,7 @@ export function patch(program: Program, entity: Type, subPath?: string) { // BUG #243: How do we deal with reserved words? export function _delete(program: Program, entity: Type, subPath?: string) { - setOperationRoute(entity, { + setOperationRoute(program, entity, { verb: "delete", subPath, }); @@ -127,13 +126,24 @@ export function _delete(program: Program, entity: Type, subPath?: string) { // -- Service-level Metadata -const serviceDetails: { +interface ServiceDetails { namespace?: NamespaceType; title?: string; version?: string; -} = {}; +} +const programServiceDetails = new WeakMap(); +function getServiceDetails(program: Program) { + let serviceDetails = programServiceDetails.get(program); + if (!serviceDetails) { + serviceDetails = {}; + programServiceDetails.set(program, serviceDetails); + } -export function _setServiceNamespace(namespace: NamespaceType): void { + return serviceDetails; +} + +export function _setServiceNamespace(program: Program, namespace: NamespaceType): void { + const serviceDetails = getServiceDetails(program); if (serviceDetails.namespace && serviceDetails.namespace !== namespace) { throwDiagnostic("Cannot set service namespace more than once in an ADL project.", namespace); } @@ -141,11 +151,13 @@ export function _setServiceNamespace(namespace: NamespaceType): void { serviceDetails.namespace = namespace; } -export function _checkIfServiceNamespace(namespace: NamespaceType): boolean { +export function _checkIfServiceNamespace(program: Program, namespace: NamespaceType): boolean { + const serviceDetails = getServiceDetails(program); return serviceDetails.namespace === namespace; } export function serviceTitle(program: Program, entity: Type, title: string) { + const serviceDetails = getServiceDetails(program); if (serviceDetails.title) { throwDiagnostic("Service title can only be set once per ADL document.", entity); } @@ -154,15 +166,17 @@ export function serviceTitle(program: Program, entity: Type, title: string) { throwDiagnostic("The @serviceTitle decorator can only be applied to namespaces.", entity); } - _setServiceNamespace(entity); + _setServiceNamespace(program, entity); serviceDetails.title = title; } -export function getServiceTitle(): string { +export function getServiceTitle(program: Program): string { + const serviceDetails = getServiceDetails(program); return serviceDetails.title || "(title)"; } export function serviceVersion(program: Program, entity: Type, version: string) { + const serviceDetails = getServiceDetails(program); // TODO: This will need to change once we support multiple service versions if (serviceDetails.version) { throwDiagnostic("Service version can only be set once per ADL document.", entity); @@ -172,47 +186,49 @@ export function serviceVersion(program: Program, entity: Type, version: string) throwDiagnostic("The @serviceVersion decorator can only be applied to namespaces.", entity); } - _setServiceNamespace(entity); + _setServiceNamespace(program, entity); serviceDetails.version = version; } -export function getServiceVersion(): string { +export function getServiceVersion(program: Program): string { + const serviceDetails = getServiceDetails(program); return serviceDetails.version || "0000-00-00"; } export function getServiceNamespaceString(program: Program): string | undefined { + const serviceDetails = getServiceDetails(program); return ( (serviceDetails.namespace && program.checker!.getNamespaceString(serviceDetails.namespace)) || undefined ); } -const producesTypes = new Map(); +const producesTypesKey = Symbol(); export function produces(program: Program, entity: Type, ...contentTypes: string[]) { if (entity.kind !== "Namespace") { throwDiagnostic("The @produces decorator can only be applied to namespaces.", entity); } - const values = getProduces(entity); - producesTypes.set(entity, values.concat(contentTypes)); + const values = getProduces(program, entity); + program.stateMap(producesTypesKey).set(entity, values.concat(contentTypes)); } -export function getProduces(entity: Type): string[] { - return producesTypes.get(entity) || []; +export function getProduces(program: Program, entity: Type): string[] { + return program.stateMap(producesTypesKey).get(entity) || []; } -const consumesTypes = new Map(); +const consumesTypesKey = Symbol(); export function consumes(program: Program, entity: Type, ...contentTypes: string[]) { if (entity.kind !== "Namespace") { throwDiagnostic("The @consumes decorator can only be applied to namespaces.", entity); } - const values = getConsumes(entity); - consumesTypes.set(entity, values.concat(contentTypes)); + const values = getConsumes(program, entity); + program.stateMap(consumesTypesKey).set(entity, values.concat(contentTypes)); } -export function getConsumes(entity: Type): string[] { - return consumesTypes.get(entity) || []; +export function getConsumes(program: Program, entity: Type): string[] { + return program.stateMap(consumesTypesKey).get(entity) || []; } diff --git a/packages/adl-vscode/src/tmlanguage.ts b/packages/adl-vscode/src/tmlanguage.ts index 153ac94ab..10b5f1c76 100644 --- a/packages/adl-vscode/src/tmlanguage.ts +++ b/packages/adl-vscode/src/tmlanguage.ts @@ -29,7 +29,7 @@ const identifierContinue = "[_$[:alnum:]]"; const beforeIdentifier = `(?=${identifierStart})`; const identifier = `\\b${identifierStart}${identifierContinue}*\\b`; const stringPattern = '\\"(?:[^\\"\\\\]|\\\\.)*\\"'; -const statementKeyword = `\\b(?:namespace|model|op|using|import)\\b`; +const statementKeyword = `\\b(?:namespace|model|op|using|import|enum|alias)\\b`; const universalEnd = `(?=,|;|@|\\)|\\}|${statementKeyword})`; const hexNumber = "\\b(? 0) { @@ -235,14 +254,19 @@ export function createChecker(program: Program) { return pendingModelType.type; } - return checkModelStatement(sym.node); + return sym.node.kind === SyntaxKind.ModelStatement + ? checkModelStatement(sym.node) + : checkAlias(sym.node); } else { - // model is templated, lets instantiate. + // declaration is templated, lets instantiate. if (!symbolLinks.declaredType) { // we haven't checked the declared type yet, so do so. - checkModelStatement(sym.node); + sym.node.kind === SyntaxKind.ModelStatement + ? checkModelStatement(sym.node) + : checkAlias(sym.node); } + if (sym.node.templateParameters!.length > node.arguments.length) { throwDiagnostic("Too few template arguments provided.", node); } @@ -285,7 +309,10 @@ export function createChecker(program: Program) { * twice at the same time, or if template parameters from more than one template * are ever in scope at once. */ - function instantiateTemplate(templateNode: ModelStatementNode, args: Type[]): ModelType { + function instantiateTemplate( + templateNode: ModelStatementNode | AliasStatementNode, + args: Type[] + ): Type { const symbolLinks = getSymbolLinks(templateNode.symbol!); const cached = symbolLinks.instantiations!.get(args) as ModelType; if (cached) { @@ -296,22 +323,31 @@ export function createChecker(program: Program) { const oldTemplate = instantiatingTemplate; templateInstantiation = args; instantiatingTemplate = templateNode; - // this cast is invalid once we support templatized `model =`. - const type = getTypeForNode(templateNode) as ModelType; + + const type = getTypeForNode(templateNode); symbolLinks.instantiations!.set(args, type); - - type.templateNode = templateNode; + if (type.kind === "Model") { + type.templateNode = templateNode; + } templateInstantiation = oldTis; instantiatingTemplate = oldTemplate; return type; } function checkUnionExpression(node: UnionExpressionNode): UnionType { + const options = node.options.flatMap((o) => { + const type = getTypeForNode(o); + if (type.kind === "Union") { + return type.options; + } + return type; + }); + return createType({ kind: "Union", node, - options: node.options.map(getTypeForNode), + options, }); } @@ -426,7 +462,7 @@ export function createChecker(program: Program) { } function getParentNamespaceType( - node: ModelStatementNode | NamespaceStatementNode | OperationStatementNode + node: ModelStatementNode | NamespaceStatementNode | OperationStatementNode | EnumStatementNode ): NamespaceType | undefined { if (!node.namespaceSymbol) return undefined; @@ -578,11 +614,7 @@ export function createChecker(program: Program) { function checkModel(node: ModelExpressionNode | ModelStatementNode) { if (node.kind === SyntaxKind.ModelStatement) { - if (node.properties) { - return checkModelStatement(node); - } else { - return checkModelEquals(node); - } + return checkModelStatement(node); } else { return checkModelExpression(node); } @@ -672,27 +704,6 @@ export function createChecker(program: Program) { return properties; } - function checkModelEquals(node: ModelStatementNode) { - // model = - // this will likely have to change, as right now `model =` is really just - // alias and so disappears. That means you can't easily rename symbols. - const assignmentType = getTypeForNode(node.assignment!); - - if (assignmentType.kind === "Model") { - const type: ModelType = createType({ - ...assignmentType, - node: node, - name: node.id.sv, - assignmentType, - namespace: getParentNamespaceType(node), - }); - - return type; - } - - return assignmentType; - } - function checkClassHeritage(heritage: ReferenceExpression[]): ModelType[] { return heritage.map((heritageRef) => { const heritageType = getTypeForNode(heritageRef); @@ -761,6 +772,57 @@ export function createChecker(program: Program) { } } + function checkAlias(node: AliasStatementNode): Type { + const links = getSymbolLinks(node.symbol!); + const instantiatingThisTemplate = instantiatingTemplate === node; + + if (links.declaredType && !instantiatingThisTemplate) { + return links.declaredType; + } + + const type = getTypeForNode(node.value); + if (!instantiatingThisTemplate) { + links.declaredType = type; + links.instantiations = new TypeInstantiationMap(); + } + + return type; + } + + function checkEnum(node: EnumStatementNode): Type { + const links = getSymbolLinks(node.symbol!); + + if (!links.type) { + const enumType: EnumType = { + kind: "Enum", + name: node.id.sv, + node, + members: [], + namespace: getParentNamespaceType(node), + }; + + node.members.map((m) => enumType.members.push(checkEnumMember(enumType, m))); + + createType(enumType); + + links.type = enumType; + } + + return links.type; + } + + function checkEnumMember(parentEnum: EnumType, node: EnumMemberNode): EnumMemberType { + const name = node.id.kind === SyntaxKind.Identifier ? node.id.sv : node.id.value; + const value = node.value ? node.value.value : undefined; + return createType({ + kind: "EnumMember", + enum: parentEnum, + name, + node, + value, + }); + } + // the types here aren't ideal and could probably be refactored. function createType(typeDef: T): T { (typeDef as any).templateArguments = templateInstantiation; @@ -772,6 +834,7 @@ export function createChecker(program: Program) { function getLiteralType(node: StringLiteralNode): StringLiteralType; function getLiteralType(node: NumericLiteralNode): NumericLiteralType; function getLiteralType(node: BooleanLiteralNode): BooleanLiteralType; + function getLiteralType(node: LiteralNode): LiteralType; function getLiteralType(node: LiteralNode): LiteralType { let type = program.literalTypes.get(node.value); if (type) { diff --git a/packages/adl/compiler/parser.ts b/packages/adl/compiler/parser.ts index 463ae5c41..24543c2ad 100644 --- a/packages/adl/compiler/parser.ts +++ b/packages/adl/compiler/parser.ts @@ -11,10 +11,13 @@ import { } from "./scanner.js"; import { ADLScriptNode, + AliasStatementNode, BooleanLiteralNode, DecoratorExpressionNode, Diagnostic, EmptyStatementNode, + EnumMemberNode, + EnumStatementNode, Expression, IdentifierNode, ImportStatementNode, @@ -127,6 +130,10 @@ namespace ListKind { toleratedDelimiter: Token.Comma, } as const; + export const EnumMembers = { + ...ModelProperties, + } as const; + const ExpresionsBase = { allowEmpty: true, delimiter: Token.Comma, @@ -169,7 +176,6 @@ export function parse(code: string | SourceFile) { let missingIdentifierCounter = 0; const parseDiagnostics: Diagnostic[] = []; const scanner = createScanner(code, reportDiagnostic); - nextToken(); return parseADLScript(); @@ -212,6 +218,13 @@ export function parse(code: string | SourceFile) { case Token.OpKeyword: item = parseOperationStatement(decorators); break; + case Token.EnumKeyword: + item = parseEnumStatement(decorators); + break; + case Token.AliasKeyword: + reportInvalidDecorators(decorators, "alias statement"); + item = parseAliasStatement(); + break; case Token.UsingKeyword: reportInvalidDecorators(decorators, "using statement"); item = parseUsingStatement(); @@ -275,6 +288,13 @@ export function parse(code: string | SourceFile) { case Token.OpKeyword: stmts.push(parseOperationStatement(decorators)); break; + case Token.EnumKeyword: + stmts.push(parseEnumStatement(decorators)); + break; + case Token.AliasKeyword: + reportInvalidDecorators(decorators, "alias statement"); + stmts.push(parseAliasStatement()); + break; case Token.UsingKeyword: reportInvalidDecorators(decorators, "using statement"); stmts.push(parseUsingStatement()); @@ -402,33 +422,18 @@ export function parse(code: string | SourceFile) { expectTokenIsOneOf(Token.OpenBrace, Token.Equals, Token.ExtendsKeyword); - if (parseOptional(Token.Equals)) { - const assignment = parseExpression(); - parseExpected(Token.Semicolon); + const heritage: ReferenceExpression[] = parseOptionalModelHeritage(); + const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); - return { - kind: SyntaxKind.ModelStatement, - id, - heritage: [], - templateParameters, - assignment, - decorators, - ...finishNode(pos), - }; - } else { - const heritage: ReferenceExpression[] = parseOptionalModelHeritage(); - const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); - - return { - kind: SyntaxKind.ModelStatement, - id, - heritage, - templateParameters, - decorators, - properties, - ...finishNode(pos), - }; - } + return { + kind: SyntaxKind.ModelStatement, + id, + heritage, + templateParameters, + decorators, + properties, + ...finishNode(pos), + }; } function parseOptionalModelHeritage() { @@ -480,7 +485,7 @@ export function parse(code: string | SourceFile) { pos: number, decorators: DecoratorExpressionNode[] ): ModelPropertyNode | ModelSpreadPropertyNode { - let id = + const id = token() === Token.StringLiteral ? parseStringLiteral() : parseIdentifier("Property expected."); @@ -499,6 +504,68 @@ export function parse(code: string | SourceFile) { }; } + function parseEnumStatement(decorators: DecoratorExpressionNode[]): EnumStatementNode { + const pos = tokenPos(); + parseExpected(Token.EnumKeyword); + const id = parseIdentifier(); + const members = parseList(ListKind.EnumMembers, parseEnumMember); + return { + kind: SyntaxKind.EnumStatement, + id, + decorators, + members, + ...finishNode(pos), + }; + } + + function parseEnumMember(pos: number, decorators: DecoratorExpressionNode[]): EnumMemberNode { + const id = + token() === Token.StringLiteral + ? parseStringLiteral() + : parseIdentifier("Enum member expected."); + + let value: StringLiteralNode | NumericLiteralNode | undefined; + if (parseOptional(Token.Colon)) { + const expr = parseExpression(); + + if (expr.kind === SyntaxKind.StringLiteral || expr.kind === SyntaxKind.NumericLiteral) { + value = expr; + } else if (getFlag(expr, NodeFlags.ThisNodeHasError)) { + parseErrorInNextFinishedNode = true; + } else { + error("Expected numeric or string literal", expr); + } + } + + return { + kind: SyntaxKind.EnumMember, + id, + value, + decorators, + ...finishNode(pos), + }; + } + + function parseAliasStatement(): AliasStatementNode { + const pos = tokenPos(); + parseExpected(Token.AliasKeyword); + const id = parseIdentifier(); + const templateParameters = parseOptionalList( + ListKind.TemplateParameters, + parseTemplateParameter + ); + parseExpected(Token.Equals); + const value = parseExpression(); + parseExpected(Token.Semicolon); + return { + kind: SyntaxKind.AliasStatement, + id, + templateParameters, + value, + ...finishNode(pos), + }; + } + function parseExpression(): Expression { return parseUnionExpressionOrHigher(); } @@ -541,7 +608,7 @@ export function parse(code: string | SourceFile) { } return { - kind: SyntaxKind.UnionExpression, + kind: SyntaxKind.IntersectionExpression, options, ...finishNode(pos), }; @@ -1063,9 +1130,20 @@ export function visitChildren(node: Node, cb: NodeCb): T | undefined { visitNode(cb, node.id) || visitEach(cb, node.templateParameters) || visitEach(cb, node.heritage) || - visitNode(cb, node.assignment) || visitEach(cb, node.properties) ); + case SyntaxKind.EnumStatement: + return ( + visitEach(cb, node.decorators) || visitNode(cb, node.id) || visitEach(cb, node.members) + ); + case SyntaxKind.EnumMember: + return visitEach(cb, node.decorators) || visitNode(cb, node.id) || visitNode(cb, node.value); + case SyntaxKind.AliasStatement: + return ( + visitNode(cb, node.id) || + visitEach(cb, node.templateParameters) || + visitNode(cb, node.value) + ); case SyntaxKind.NamedImport: return visitNode(cb, node.id); case SyntaxKind.TypeReference: diff --git a/packages/adl/compiler/program.ts b/packages/adl/compiler/program.ts index 1963f74db..0ab62dbcb 100644 --- a/packages/adl/compiler/program.ts +++ b/packages/adl/compiler/program.ts @@ -32,6 +32,8 @@ export interface Program { executeModelDecorators(type: ModelType): void; executeDecorators(type: Type): void; executeDecorator(node: DecoratorExpressionNode, program: Program, type: Type): void; + stateSet(key: Symbol): Set; + stateMap(key: Symbol): Map; } export async function createProgram( @@ -39,6 +41,8 @@ export async function createProgram( options: CompilerOptions ): Promise { const buildCbs: any = []; + const stateMaps = new Map>(); + const stateSets = new Map>(); const seenSourceFiles = new Set(); const program: Program = { @@ -52,6 +56,8 @@ export async function createProgram( executeDecorators, executeDecorator, getOption, + stateMap, + stateSet, onBuild(cb) { buildCbs.push(cb); }, @@ -142,7 +148,7 @@ export async function createProgram( * just the raw type objects, but literals are treated specially. */ function toJSON(type: Type): Type | string | number | boolean { - if ("value" in type) { + if (type.kind === "Number" || type.kind === "String" || type.kind === "Boolean") { return type.value; } @@ -355,6 +361,26 @@ export async function createProgram( function getOption(key: string): string | undefined { return (options.miscOptions || {})[key]; } + + function stateMap(key: Symbol): Map { + let m = stateMaps.get(key); + if (!m) { + m = new Map(); + stateMaps.set(key, m); + } + + return m; + } + + function stateSet(key: Symbol): Set { + let s = stateSets.get(key); + if (!s) { + s = new Set(); + stateSets.set(key, s); + } + + return s; + } } export async function compile(rootDir: string, host: CompilerHost, options?: CompilerOptions) { diff --git a/packages/adl/compiler/scanner.ts b/packages/adl/compiler/scanner.ts index 58243b3e9..c2cfcaaed 100644 --- a/packages/adl/compiler/scanner.ts +++ b/packages/adl/compiler/scanner.ts @@ -58,6 +58,7 @@ export enum Token { Question = 25, Colon = 26, At = 27, + // Update MaxPunctuation if anything is added right above here // Identifiers Identifier = 28, @@ -68,11 +69,15 @@ export enum Token { NamespaceKeyword = 31, UsingKeyword = 32, OpKeyword = 33, + EnumKeyword = 34, + AliasKeyword = 35, + // Update MaxStatementKeyword if anything is added right above here // Other keywords - ExtendsKeyword = 34, - TrueKeyword = 35, - FalseKeyword = 36, + ExtendsKeyword = 36, + TrueKeyword = 37, + FalseKeyword = 38, + // Update MaxKeyword if anything is added right above here } const MinKeyword = Token.ImportKeyword; @@ -82,7 +87,7 @@ const MinPunctuation = Token.OpenBrace; const MaxPunctuation = Token.At; const MinStatementKeyword = Token.ImportKeyword; -const MaxStatementKeyword = Token.OpKeyword; +const MaxStatementKeyword = Token.AliasKeyword; /** @internal */ export const TokenDisplay: readonly string[] = [ @@ -120,6 +125,8 @@ export const TokenDisplay: readonly string[] = [ "'namespace'", "'using'", "'op'", + "'enum'", + "'alias'", "'extends'", "'true'", "'false'", @@ -133,6 +140,8 @@ export const Keywords: ReadonlyMap = new Map([ ["using", Token.UsingKeyword], ["op", Token.OpKeyword], ["extends", Token.ExtendsKeyword], + ["enum", Token.EnumKeyword], + ["alias", Token.AliasKeyword], ["true", Token.TrueKeyword], ["false", Token.FalseKeyword], ]); @@ -141,7 +150,7 @@ export const Keywords: ReadonlyMap = new Map([ export const enum KeywordLimit { MinLength = 2, MaxLength = 9, - MinStartChar = CharCode.e, + MinStartChar = CharCode.a, MaxStartChar = CharCode.u, } diff --git a/packages/adl/compiler/types.ts b/packages/adl/compiler/types.ts index 8b95ff9c5..e3500d369 100644 --- a/packages/adl/compiler/types.ts +++ b/packages/adl/compiler/types.ts @@ -10,6 +10,8 @@ export interface BaseType { export type Type = | ModelType | ModelTypeProperty + | EnumType + | EnumMemberType | TemplateParameterType | NamespaceType | OperationType @@ -35,7 +37,6 @@ export interface ModelType extends BaseType { baseModels: ModelType[]; templateArguments?: Type[]; templateNode?: Node; - assignmentType?: Type; } export interface ModelTypeProperty { @@ -49,6 +50,22 @@ export interface ModelTypeProperty { optional: boolean; } +export interface EnumType extends BaseType { + kind: "Enum"; + name: string; + node: EnumStatementNode; + namespace?: NamespaceType; + members: EnumMemberType[]; +} + +export interface EnumMemberType extends BaseType { + kind: "EnumMember"; + name: string; + enum: EnumType; + node: EnumMemberNode; + value?: string | number; +} + export interface OperationType { kind: "Operation"; node: OperationStatementNode; @@ -166,6 +183,9 @@ export enum SyntaxKind { ModelExpression, ModelProperty, ModelSpreadProperty, + EnumStatement, + EnumMember, + AliasStatement, UnionExpression, IntersectionExpression, TupleExpression, @@ -190,7 +210,7 @@ export type Node = | ModelPropertyNode | OperationStatementNode | NamedImportNode - | ModelPropertyNode + | EnumMemberNode | ModelSpreadPropertyNode | DecoratorExpressionNode | Statement @@ -214,6 +234,8 @@ export type Statement = | ModelStatementNode | NamespaceStatementNode | UsingStatementNode + | EnumStatementNode + | AliasStatementNode | OperationStatementNode | EmptyStatementNode | InvalidStatementNode; @@ -227,9 +249,15 @@ export type Declaration = | ModelStatementNode | NamespaceStatementNode | OperationStatementNode - | TemplateParameterDeclarationNode; + | TemplateParameterDeclarationNode + | EnumStatementNode + | AliasStatementNode; -export type ScopeNode = NamespaceStatementNode | ModelStatementNode | ADLScriptNode; +export type ScopeNode = + | NamespaceStatementNode + | ModelStatementNode + | AliasStatementNode + | ADLScriptNode; export interface ImportStatementNode extends BaseNode { kind: SyntaxKind.ImportStatement; @@ -300,12 +328,33 @@ export interface ModelStatementNode extends BaseNode, DeclarationNode { id: IdentifierNode; properties?: (ModelPropertyNode | ModelSpreadPropertyNode)[]; heritage: ReferenceExpression[]; - assignment?: Expression; templateParameters: TemplateParameterDeclarationNode[]; locals?: SymbolTable; decorators: DecoratorExpressionNode[]; } +export interface EnumStatementNode extends BaseNode, DeclarationNode { + kind: SyntaxKind.EnumStatement; + id: IdentifierNode; + members: EnumMemberNode[]; + decorators: DecoratorExpressionNode[]; +} + +export interface EnumMemberNode extends BaseNode { + kind: SyntaxKind.EnumMember; + id: IdentifierNode | StringLiteralNode; + value?: StringLiteralNode | NumericLiteralNode; + decorators: DecoratorExpressionNode[]; +} + +export interface AliasStatementNode extends BaseNode, DeclarationNode { + kind: SyntaxKind.AliasStatement; + id: IdentifierNode; + value: Expression; + templateParameters: TemplateParameterDeclarationNode[]; + locals?: SymbolTable; +} + export interface InvalidStatementNode extends BaseNode { kind: SyntaxKind.InvalidStatement; } diff --git a/packages/adl/lib/decorators.ts b/packages/adl/lib/decorators.ts index c8de12ba9..7cb5ccfa8 100644 --- a/packages/adl/lib/decorators.ts +++ b/packages/adl/lib/decorators.ts @@ -2,14 +2,13 @@ import { throwDiagnostic } from "../compiler/diagnostics.js"; import { Program } from "../compiler/program.js"; import { ModelTypeProperty, NamespaceType, Type } from "../compiler/types.js"; -const docs = new Map(); - +const docsKey = Symbol(); export function doc(program: Program, target: Type, text: string) { - docs.set(target, text); + program.stateMap(docsKey).set(target, text); } -export function getDoc(target: Type) { - return docs.get(target); +export function getDoc(program: Program, target: Type): string { + return program.stateMap(docsKey).get(target); } export function inspectType(program: Program, target: Type, text: string) { @@ -22,26 +21,30 @@ export function inspectTypeName(program: Program, target: Type, text: string) { console.log(program.checker!.getTypeName(target)); } -const intrinsics = new Set(); +const intrinsicsKey = Symbol(); export function intrinsic(program: Program, target: Type) { - intrinsics.add(target); + program.stateSet(intrinsicsKey).add(target); } -export function isIntrinsic(target: Type) { - return intrinsics.has(target); +export function isIntrinsic(program: Program, target: Type) { + return program.stateSet(intrinsicsKey).has(target); } // Walks the assignmentType chain to find the core intrinsic type, if any -export function getIntrinsicType(target: Type | undefined): string | undefined { +export function getIntrinsicType(program: Program, target: Type | undefined): string | undefined { while (target) { if (target.kind === "Model") { - if (isIntrinsic(target)) { + if (isIntrinsic(program, target)) { return target.name; } - target = (target.assignmentType?.kind === "Model" && target.assignmentType) || undefined; + if (target.baseModels.length === 1) { + target = target.baseModels[0]; + } else { + target = undefined; + } } else if (target.kind === "ModelProperty") { - return getIntrinsicType(target.type); + return getIntrinsicType(program, target.type); } else { break; } @@ -50,33 +53,32 @@ export function getIntrinsicType(target: Type | undefined): string | undefined { return undefined; } -const numericTypes = new Set(); - +const numericTypesKey = Symbol(); export function numeric(program: Program, target: Type) { - if (!isIntrinsic(target)) { + if (!isIntrinsic(program, target)) { throwDiagnostic("Cannot apply @numeric decorator to non-intrinsic type.", target); } if (target.kind === "Model") { - numericTypes.add(target.name); + program.stateSet(numericTypesKey).add(target.name); } else { throwDiagnostic("Cannot apply @numeric decorator to non-model type.", target); } } -export function isNumericType(target: Type): boolean { - const intrinsicType = getIntrinsicType(target); - return intrinsicType !== undefined && numericTypes.has(intrinsicType); +export function isNumericType(program: Program, target: Type): boolean { + const intrinsicType = getIntrinsicType(program, target); + return intrinsicType !== undefined && program.stateSet(numericTypesKey).has(intrinsicType); } // -- @format decorator --------------------- -const formatValues = new Map(); +const formatValuesKey = Symbol(); export function format(program: Program, target: Type, format: string) { if (target.kind === "Model" || target.kind === "ModelProperty") { // Is it a model type that ultimately derives from 'string'? - if (getIntrinsicType(target) === "string") { - formatValues.set(target, format); + if (getIntrinsicType(program, target) === "string") { + program.stateMap(formatValuesKey).set(target, format); } else { throwDiagnostic("Cannot apply @format to a non-string type", target); } @@ -85,19 +87,19 @@ export function format(program: Program, target: Type, format: string) { } } -export function getFormat(target: Type): string | undefined { - return formatValues.get(target); +export function getFormat(program: Program, target: Type): string | undefined { + return program.stateMap(formatValuesKey).get(target); } // -- @minLength decorator --------------------- -const minLengthValues = new Map(); +const minLengthValuesKey = Symbol(); export function minLength(program: Program, target: Type, minLength: number) { if (target.kind === "Model" || target.kind === "ModelProperty") { // Is it a model type that ultimately derives from 'string'? - if (getIntrinsicType(target) === "string") { - minLengthValues.set(target, minLength); + if (getIntrinsicType(program, target) === "string") { + program.stateMap(minLengthValuesKey).set(target, minLength); } else { throwDiagnostic("Cannot apply @minLength to a non-string type", target); } @@ -109,19 +111,19 @@ export function minLength(program: Program, target: Type, minLength: number) { } } -export function getMinLength(target: Type): number | undefined { - return minLengthValues.get(target); +export function getMinLength(program: Program, target: Type): number | undefined { + return program.stateMap(minLengthValuesKey).get(target); } // -- @maxLength decorator --------------------- -const maxLengthValues = new Map(); +const maxLengthValuesKey = Symbol(); export function maxLength(program: Program, target: Type, maxLength: number) { if (target.kind === "Model" || target.kind === "ModelProperty") { // Is it a model type that ultimately derives from 'string'? - if (getIntrinsicType(target) === "string") { - maxLengthValues.set(target, maxLength); + if (getIntrinsicType(program, target) === "string") { + program.stateMap(maxLengthValuesKey).set(target, maxLength); } else { throwDiagnostic("Cannot apply @maxLength to a non-string type", target); } @@ -133,19 +135,19 @@ export function maxLength(program: Program, target: Type, maxLength: number) { } } -export function getMaxLength(target: Type): number | undefined { - return maxLengthValues.get(target); +export function getMaxLength(program: Program, target: Type): number | undefined { + return program.stateMap(maxLengthValuesKey).get(target); } // -- @minValue decorator --------------------- -const minValues = new Map(); +const minValuesKey = Symbol(); export function minValue(program: Program, target: Type, minValue: number) { if (target.kind === "Model" || target.kind === "ModelProperty") { // Is it ultimately a numeric type? - if (isNumericType(target)) { - minValues.set(target, minValue); + if (isNumericType(program, target)) { + program.stateMap(minValuesKey).set(target, minValue); } else { throwDiagnostic("Cannot apply @minValue to a non-numeric type", target); } @@ -157,19 +159,19 @@ export function minValue(program: Program, target: Type, minValue: number) { } } -export function getMinValue(target: Type): number | undefined { - return minValues.get(target); +export function getMinValue(program: Program, target: Type): number | undefined { + return program.stateMap(minValuesKey).get(target); } // -- @maxValue decorator --------------------- -const maxValues = new Map(); +const maxValuesKey = Symbol(); export function maxValue(program: Program, target: Type, maxValue: number) { if (target.kind === "Model" || target.kind === "ModelProperty") { // Is it ultimately a numeric type? - if (isNumericType(target)) { - maxValues.set(target, maxValue); + if (isNumericType(program, target)) { + program.stateMap(maxValuesKey).set(target, maxValue); } else { throwDiagnostic("Cannot apply @maxValue to a non-numeric type", target); } @@ -181,19 +183,19 @@ export function maxValue(program: Program, target: Type, maxValue: number) { } } -export function getMaxValue(target: Type): number | undefined { - return maxValues.get(target); +export function getMaxValue(program: Program, target: Type): number | undefined { + return program.stateMap(maxValuesKey).get(target); } // -- @secret decorator --------------------- -const secretTypes = new Map(); +const secretTypesKey = Symbol(); export function secret(program: Program, target: Type) { if (target.kind === "Model") { // Is it a model type that ultimately derives from 'string'? - if (getIntrinsicType(target) === "string") { - secretTypes.set(target, true); + if (getIntrinsicType(program, target) === "string") { + program.stateMap(secretTypesKey).set(target, true); } else { throwDiagnostic("Cannot apply @secret to a non-string type", target); } @@ -202,24 +204,24 @@ export function secret(program: Program, target: Type) { } } -export function isSecret(target: Type): boolean | undefined { - return secretTypes.get(target); +export function isSecret(program: Program, target: Type): boolean | undefined { + return program.stateMap(secretTypesKey).get(target); } // -- @visibility decorator --------------------- -const visibilitySettings = new Map(); +const visibilitySettingsKey = Symbol(); export function visibility(program: Program, target: Type, ...visibilities: string[]) { if (target.kind === "ModelProperty") { - visibilitySettings.set(target, visibilities); + program.stateMap(visibilitySettingsKey).set(target, visibilities); } else { throwDiagnostic("The @visibility decorator can only be applied to model properties.", target); } } -export function getVisibility(target: Type): string[] | undefined { - return visibilitySettings.get(target); +export function getVisibility(program: Program, target: Type): string[] | undefined { + return program.stateMap(visibilitySettingsKey).get(target); } export function withVisibility(program: Program, target: Type, ...visibilities: string[]) { @@ -228,7 +230,7 @@ export function withVisibility(program: Program, target: Type, ...visibilities: } const filter = (_: any, prop: ModelTypeProperty) => { - const vis = getVisibility(prop); + const vis = getVisibility(program, prop); return vis !== undefined && visibilities.filter((v) => !vis.includes(v)).length > 0; }; @@ -248,11 +250,11 @@ function mapFilterOut( // -- @list decorator --------------------- -const listProperties = new Set(); +const listPropertiesKey = Symbol(); export function list(program: Program, target: Type) { if (target.kind === "Operation" || target.kind === "ModelProperty") { - listProperties.add(target); + program.stateSet(listPropertiesKey).add(target); } else { throwDiagnostic( "The @list decorator can only be applied to interface or model properties.", @@ -261,22 +263,22 @@ export function list(program: Program, target: Type) { } } -export function isList(target: Type): boolean { - return listProperties.has(target); +export function isList(program: Program, target: Type): boolean { + return program.stateSet(listPropertiesKey).has(target); } // -- @tag decorator --------------------- -const tagProperties = new Map(); +const tagPropertiesKey = Symbol(); // Set a tag on an operation or namespace. There can be multiple tags on either an // operation or namespace. export function tag(program: Program, target: Type, tag: string) { if (target.kind === "Operation" || target.kind === "Namespace") { - const tags = tagProperties.get(target); + const tags = program.stateMap(tagPropertiesKey).get(target); if (tags) { tags.push(tag); } else { - tagProperties.set(target, [tag]); + program.stateMap(tagPropertiesKey).set(target, [tag]); } } else { throwDiagnostic("The @tag decorator can only be applied to namespace or operation.", target); @@ -284,20 +286,24 @@ export function tag(program: Program, target: Type, tag: string) { } // Return the tags set on an operation or namespace -export function getTags(target: Type): string[] { - return tagProperties.get(target) || []; +export function getTags(program: Program, target: Type): string[] { + return program.stateMap(tagPropertiesKey).get(target) || []; } // Merge the tags for a operation with the tags that are on the namespace it resides within. // // TODO: (JC) We'll need to update this for nested namespaces -export function getAllTags(namespace: NamespaceType, target: Type): string[] | undefined { +export function getAllTags( + program: Program, + namespace: NamespaceType, + target: Type +): string[] | undefined { const tags = new Set(); - for (const t of getTags(namespace)) { + for (const t of getTags(program, namespace)) { tags.add(t); } - for (const t of getTags(target)) { + for (const t of getTags(program, target)) { tags.add(t); } return tags.size > 0 ? Array.from(tags) : undefined; diff --git a/packages/adl/test/checker/alias.ts b/packages/adl/test/checker/alias.ts new file mode 100644 index 000000000..73aedb865 --- /dev/null +++ b/packages/adl/test/checker/alias.ts @@ -0,0 +1,165 @@ +import { ok, strictEqual } from "assert"; +import { ModelType, UnionType } from "../../compiler/types.js"; +import { createTestHost, TestHost } from "../test-host.js"; + +describe("aliases", () => { + let testHost: TestHost; + + beforeEach(async () => { + testHost = await createTestHost(); + }); + + it("can alias a union expression", async () => { + testHost.addAdlFile( + "a.adl", + ` + alias Foo = int32 | string; + alias Bar = "hi" | 10; + alias FooBar = Foo | Bar; + + @test model A { + prop: FooBar + } + ` + ); + const { A } = (await testHost.compile("./")) as { + A: ModelType; + }; + + const propType: UnionType = A.properties.get("prop")!.type as UnionType; + strictEqual(propType.kind, "Union"); + strictEqual(propType.options.length, 4); + strictEqual(propType.options[0].kind, "Model"); + strictEqual(propType.options[1].kind, "Model"); + strictEqual(propType.options[2].kind, "String"); + strictEqual(propType.options[3].kind, "Number"); + }); + + it("can alias a deep union expression", async () => { + testHost.addAdlFile( + "a.adl", + ` + alias Foo = int32 | string; + alias Bar = "hi" | 10; + alias Baz = Foo | Bar; + alias FooBar = Baz | "bye"; + + @test model A { + prop: FooBar + } + ` + ); + const { A } = (await testHost.compile("./")) as { + A: ModelType; + }; + + const propType: UnionType = A.properties.get("prop")!.type as UnionType; + strictEqual(propType.kind, "Union"); + strictEqual(propType.options.length, 5); + strictEqual(propType.options[0].kind, "Model"); + strictEqual(propType.options[1].kind, "Model"); + strictEqual(propType.options[2].kind, "String"); + strictEqual(propType.options[3].kind, "Number"); + strictEqual(propType.options[4].kind, "String"); + }); + + it("can alias a union expression with parameters", async () => { + testHost.addAdlFile( + "a.adl", + ` + alias Foo = int32 | T; + + @test model A { + prop: Foo<"hi"> + } + ` + ); + + const { A } = (await testHost.compile("./")) as { + A: ModelType; + }; + + const propType: UnionType = A.properties.get("prop")!.type as UnionType; + strictEqual(propType.kind, "Union"); + strictEqual(propType.options.length, 2); + strictEqual(propType.options[0].kind, "Model"); + strictEqual(propType.options[1].kind, "String"); + }); + + it("can alias a deep union expression with parameters", async () => { + testHost.addAdlFile( + "a.adl", + ` + alias Foo = int32 | T; + alias Bar = Foo | Foo; + + @test model A { + prop: Bar<"hi", 42> + } + ` + ); + + const { A } = (await testHost.compile("./")) as { + A: ModelType; + }; + + const propType: UnionType = A.properties.get("prop")!.type as UnionType; + strictEqual(propType.kind, "Union"); + strictEqual(propType.options.length, 4); + strictEqual(propType.options[0].kind, "Model"); + strictEqual(propType.options[1].kind, "String"); + strictEqual(propType.options[2].kind, "Model"); + strictEqual(propType.options[3].kind, "Number"); + }); + + it("can alias an intersection expression", async () => { + testHost.addAdlFile( + "a.adl", + ` + alias Foo = {a: string} & {b: string}; + alias Bar = {c: string} & {d: string}; + alias FooBar = Foo & Bar; + + @test model A { + prop: FooBar + } + ` + ); + const { A } = (await testHost.compile("./")) as { + A: ModelType; + }; + + const propType: ModelType = A.properties.get("prop")!.type as ModelType; + strictEqual(propType.kind, "Model"); + strictEqual(propType.properties.size, 4); + ok(propType.properties.has("a")); + ok(propType.properties.has("b")); + ok(propType.properties.has("c")); + ok(propType.properties.has("d")); + }); + + it("can be used like any model", async () => { + testHost.addAdlFile( + "a.adl", + ` + @test model Test { a: string }; + + alias Alias = Test; + + @test model A extends Alias { }; + @test model B { ... Alias }; + @test model C { c: Alias }; + ` + ); + const { Test, A, B, C } = (await testHost.compile("./")) as { + Test: ModelType; + A: ModelType; + B: ModelType; + C: ModelType; + }; + + strictEqual(A.baseModels[0], Test); + ok(B.properties.has("a")); + strictEqual(C.properties.get("c")!.type, Test); + }); +}); diff --git a/packages/adl/test/checker/enum.ts b/packages/adl/test/checker/enum.ts new file mode 100644 index 000000000..db2fd819e --- /dev/null +++ b/packages/adl/test/checker/enum.ts @@ -0,0 +1,76 @@ +import { ok, strictEqual } from "assert"; +import { EnumMemberType, EnumType, ModelType } from "../../compiler/types.js"; +import { createTestHost, TestHost } from "../test-host.js"; + +describe("enums", () => { + let testHost: TestHost; + + beforeEach(async () => { + testHost = await createTestHost(); + }); + + it("can be valueless", async () => { + testHost.addAdlFile( + "a.adl", + ` + @test enum E { + A, B, C + } + ` + ); + + const { E } = (await testHost.compile("./")) as { + E: EnumType; + }; + + ok(E); + ok(!E.members[0].value); + ok(!E.members[1].value); + ok(!E.members[2].value); + }); + + it("can have values", async () => { + testHost.addAdlFile( + "a.adl", + ` + @test enum E { + @test("A") A: "a"; + @test("B") B: "b"; + @test("C") C: "c"; + } + ` + ); + + const { E, A, B, C } = (await testHost.compile("./")) as { + E: EnumType; + A: EnumMemberType; + B: EnumMemberType; + C: EnumMemberType; + }; + + ok(E); + strictEqual(A.value, "a"); + strictEqual(B.value, "b"); + strictEqual(C.value, "c"); + }); + + it("can be a model property", async () => { + testHost.addAdlFile( + "a.adl", + ` + namespace Foo; + enum E { A, B, C } + @test model Foo { + prop: E; + } + ` + ); + + const { Foo } = (await testHost.compile("./")) as { + Foo: ModelType; + }; + + ok(Foo); + strictEqual(Foo.properties.get("prop")!.type.kind, "Enum"); + }); +}); diff --git a/packages/adl/test/checker/namespaces.ts b/packages/adl/test/checker/namespaces.ts index 5ddad4a07..2f4a6e4db 100644 --- a/packages/adl/test/checker/namespaces.ts +++ b/packages/adl/test/checker/namespaces.ts @@ -326,7 +326,7 @@ describe("blockless namespaces", () => { "a.adl", ` import "./b.adl"; - model M = N.X; + model M {x: N.X } ` ); testHost.addAdlFile( diff --git a/packages/adl/test/checker/using.ts b/packages/adl/test/checker/using.ts index 050e23a85..0bab618a2 100644 --- a/packages/adl/test/checker/using.ts +++ b/packages/adl/test/checker/using.ts @@ -167,7 +167,7 @@ describe("using statements", () => { } namespace M { - model X = A; + model X { a: A }; } ` ); diff --git a/packages/adl/test/decorators/range-limits.ts b/packages/adl/test/decorators/range-limits.ts index 6ada04b53..493f2aed7 100644 --- a/packages/adl/test/decorators/range-limits.ts +++ b/packages/adl/test/decorators/range-limits.ts @@ -21,9 +21,9 @@ describe("range limiting decorators", () => { const { A, B } = (await testHost.compile("./")) as { A: ModelType; B: ModelType }; - strictEqual(getMinValue(A.properties.get("foo")!), 15); - strictEqual(getMaxValue(A.properties.get("boo")!), 55); - strictEqual(getMaxValue(B.properties.get("bar")!), 20); - strictEqual(getMinValue(B.properties.get("car")!), 23); + strictEqual(getMinValue(testHost.program, A.properties.get("foo")!), 15); + strictEqual(getMaxValue(testHost.program, A.properties.get("boo")!), 55); + strictEqual(getMaxValue(testHost.program, B.properties.get("bar")!), 20); + strictEqual(getMinValue(testHost.program, B.properties.get("car")!), 23); }); }); diff --git a/packages/adl/test/libraries/simple/main.adl b/packages/adl/test/libraries/simple/main.adl index 6c0e564db..2ab41a9b3 100644 --- a/packages/adl/test/libraries/simple/main.adl +++ b/packages/adl/test/libraries/simple/main.adl @@ -2,5 +2,5 @@ import "MyLib"; import "CustomAdlMain"; @myLibDec -model A = MyLib.Model; -model B = CustomAdlMain.Model; +model A { x: MyLib.Model }; +model B { x: CustomAdlMain.Model }; diff --git a/packages/adl/test/test-host.ts b/packages/adl/test/test-host.ts index 9e073cb2c..758e38f4a 100644 --- a/packages/adl/test/test-host.ts +++ b/packages/adl/test/test-host.ts @@ -1,14 +1,19 @@ import { readdir, readFile } from "fs/promises"; import { basename, isAbsolute, join, normalize, relative, resolve, sep } from "path"; import { fileURLToPath, pathToFileURL } from "url"; +import { CompilerOptions } from "../compiler/options"; +import { Program } from "../compiler/program"; import { createProgram } from "../compiler/program.js"; import { CompilerHost, Type } from "../compiler/types"; export interface TestHost { addAdlFile(path: string, contents: string): void; addJsFile(path: string, contents: any): void; - compile(main: string): Promise>; + addRealAdlFile(path: string, realPath: string): Promise; + addRealJsFile(path: string, realPath: string): Promise; + compile(main: string, options?: CompilerOptions): Promise>; testTypes: Record; + program: Program; /** * Virtual filesystem used in the tests. */ @@ -17,7 +22,7 @@ export interface TestHost { export async function createTestHost(): Promise { const testTypes: Record = {}; - + let program: Program = undefined as any; // in practice it will always be initialized const virtualFs: { [name: string]: string } = {}; const jsImports: { [path: string]: Promise } = {}; const compilerHost: CompilerHost = { @@ -66,6 +71,17 @@ export async function createTestHost(): Promise { }, async stat(path: string) { + if (virtualFs.hasOwnProperty(path)) { + return { + isDirectory() { + return false; + }, + isFile() { + return true; + }, + }; + } + for (const fsPath of Object.keys(virtualFs)) { if (fsPath.startsWith(path) && fsPath !== path) { return { @@ -79,14 +95,7 @@ export async function createTestHost(): Promise { } } - return { - isDirectory() { - return false; - }, - isFile() { - return true; - }, - }; + throw { code: "ENOENT" }; }, // symlinks not supported in test-host @@ -120,7 +129,7 @@ export async function createTestHost(): Promise { addJsFile("/.adl/test-lib/test.js", { test(_: any, target: Type, name?: string) { if (!name) { - if (target.kind === "Model" || target.kind === "Namespace") { + if (target.kind === "Model" || target.kind === "Namespace" || target.kind === "Enum") { name = target.name; } else { throw new Error("Need to specify a name for test type"); @@ -134,8 +143,13 @@ export async function createTestHost(): Promise { return { addAdlFile, addJsFile, + addRealAdlFile, + addRealJsFile, compile, testTypes, + get program() { + return program; + }, fs: virtualFs, }; @@ -150,15 +164,35 @@ export async function createTestHost(): Promise { jsImports[key] = new Promise((r) => r(contents)); } - async function compile(main: string) { + async function addRealAdlFile(path: string, existingPath: string) { + virtualFs[resolve(compilerHost.getCwd(), path)] = await readFile(existingPath, "utf8"); + } + + async function addRealJsFile(path: string, existingPath: string) { + const key = resolve(compilerHost.getCwd(), path); + const exports = await import(pathToFileURL(existingPath).href); + + virtualFs[key] = ""; + jsImports[key] = exports; + } + + async function compile(main: string, options: CompilerOptions = {}) { + // default is noEmit + if (!options.hasOwnProperty("noEmit")) { + options.noEmit = true; + } + try { - const program = await createProgram(compilerHost, { + program = await createProgram(compilerHost, { mainFile: main, - noEmit: true, + ...options, }); return testTypes; } catch (e) { + if (e.diagnostics) { + throw e.diagnostics; + } throw e; } } diff --git a/packages/adl/test/test-parser.ts b/packages/adl/test/test-parser.ts index f64ae864b..5186d1a43 100644 --- a/packages/adl/test/test-parser.ts +++ b/packages/adl/test/test-parser.ts @@ -101,7 +101,11 @@ describe("syntax", () => { }); describe("model = statements", () => { - parseEach(["model x = y;", "model foo = bar | baz;", "model bar = a | b;"]); + parseErrorEach([ + ["model x = y;", [/'{' expected/]], + ["model foo = bar | baz;", [/'{' expected/]], + ["model bar = a | b;", [/'{' expected/]], + ]); }); describe("model expressions", () => { @@ -121,7 +125,7 @@ describe("syntax", () => { }); describe("template instantiations", () => { - parseEach(["model A = Foo;", "model B = Foo[];"]); + parseEach(["model A { x: Foo; }", "model B { x: Foo[]; }"]); }); describe("intersection expressions", () => { @@ -129,7 +133,7 @@ describe("syntax", () => { }); describe("parenthesized expressions", () => { - parseEach(["model A = ((B | C) & D)[];"]); + parseEach(["model A { x: ((B | C) & D)[]; }"]); }); describe("namespace statements", () => { @@ -165,7 +169,6 @@ describe("syntax", () => { ` model A { }; model B { } - model C = A; ; namespace I { op foo(): number; @@ -221,25 +224,25 @@ describe("syntax", () => { describe("unterminated tokens", () => { parseErrorEach([ - ['model X = "banana', [/Unterminated string literal/]], - ['model X = "banana\\', [/Unterminated string literal/]], - ['model X = """\nbanana', [/Unterminated string literal/]], - ['model X = """\nbanana\\', [/Unterminated string literal/]], + ['alias X = "banana', [/Unterminated string literal/]], + ['alias X = "banana\\', [/Unterminated string literal/]], + ['alias X = """\nbanana', [/Unterminated string literal/]], + ['alias X = """\nbanana\\', [/Unterminated string literal/]], ["/* Yada yada yada", [/Unterminated comment/]], ]); }); describe("terminated tokens at EOF with missing semicolon", () => { parseErrorEach([ - ["model X = 0x10101", [/';' expected/]], - ["model X = 0xBEEF", [/';' expected/]], - ["model X = 123", [/';' expected/]], - ["model X = 123e45", [/';' expected/]], - ["model X = 123.45", [/';' expected/]], - ["model X = 123.45e2", [/';' expected/]], - ["model X = Banana", [/';' expected/]], - ['model X = "Banana"', [/';' expected/]], - ['model X = """\nBanana\n"""', [/';' expected/]], + ["alias X = 0x10101", [/';' expected/]], + ["alias X = 0xBEEF", [/';' expected/]], + ["alias X = 123", [/';' expected/]], + ["alias X = 123e45", [/';' expected/]], + ["alias X = 123.45", [/';' expected/]], + ["alias X = 123.45e2", [/';' expected/]], + ["alias X = Banana", [/';' expected/]], + ['alias X = "Banana"', [/';' expected/]], + ['alias X = """\nBanana\n"""', [/';' expected/]], ]); }); @@ -290,13 +293,13 @@ describe("syntax", () => { ["0xG", /Hexadecimal digit expected/], ]; - parseEach(good.map((c) => [`model M = ${c[0]};`, (node) => isNumericLiteral(node, c[1])])); - parseErrorEach(bad.map((c) => [`model M = ${c[0]};`, [c[1]]])); + parseEach(good.map((c) => [`alias M = ${c[0]};`, (node) => isNumericLiteral(node, c[1])])); + parseErrorEach(bad.map((c) => [`alias M = ${c[0]};`, [c[1]]])); function isNumericLiteral(node: ADLScriptNode, value: number) { const statement = node.statements[0]; - assert(statement.kind === SyntaxKind.ModelStatement, "model statement expected"); - const assignment = statement.assignment; + assert(statement.kind === SyntaxKind.AliasStatement, "alias statement expected"); + const assignment = statement.value; assert(assignment?.kind === SyntaxKind.NumericLiteral, "numeric literal expected"); assert.strictEqual(assignment.value, value); } @@ -312,6 +315,33 @@ describe("syntax", () => { ]); parseErrorEach([["model 😢 {}", [/Invalid character/]]]); }); + + describe("enum statements", () => { + parseEach([ + "enum Foo { }", + "enum Foo { a, b }", + 'enum Foo { a: "hi", c: 10 }', + "@foo enum Foo { @bar a, @baz b: 10 }", + ]); + + parseErrorEach([ + ["enum Foo { a: number }", [/Expected numeric or string literal/]], + ["enum Foo { a: ; b: ; }", [/Expression expected/, /Expression expected/]], + ["enum Foo { ;+", [/Enum member expected/]], + ["enum { }", [/Identifier expected/]], + ]); + }); + + describe("alias statements", () => { + parseEach(["alias X = 1;", "alias X = A | B;", "alias MaybeUndefined = T | undefined;"]); + parseErrorEach([ + ["@foo alias Bar = 1;", [/Cannot decorate alias statement/]], + ["alias Foo =", [/Expression expected/]], + ["alias Foo<> =", [/Identifier expected/, /Expression expected/]], + ["alias Foo = X |", [/Expression expected/]], + ["alias =", [/Identifier expected/]], + ]); + }); }); function parseEach(cases: (string | [string, (node: ADLScriptNode) => void])[]) { @@ -359,7 +389,7 @@ function parseErrorEach(cases: [string, RegExp[]][]) { logVerboseTestOutput("\n=== Diagnostics ==="); logVerboseTestOutput((log) => logDiagnostics(astNode.parseDiagnostics, log)); - assert.notStrictEqual(astNode.parseDiagnostics.length, 0); + assert.notStrictEqual(astNode.parseDiagnostics.length, 0, "parse diagnostics length"); let i = 0; for (const match of matches) { assert.match(astNode.parseDiagnostics[i++].message, match); diff --git a/packages/adl/test/test-scanner.ts b/packages/adl/test/test-scanner.ts index 07404ca94..cb293de01 100644 --- a/packages/adl/test/test-scanner.ts +++ b/packages/adl/test/test-scanner.ts @@ -123,6 +123,11 @@ describe("scanner", () => { ]); }); + it("scans intersections", () => { + const all = tokens("A&B"); + verify(all, [[Token.Identifier, "A"], [Token.Ampersand], [Token.Identifier, "B"]]); + }); + it("scans decorator expressions", () => { const all = tokens('@foo(1,"hello",foo)'); @@ -274,10 +279,26 @@ describe("scanner", () => { } } - assert.strictEqual(minKeywordLengthFound, KeywordLimit.MinLength); - assert.strictEqual(maxKeywordLengthFound, KeywordLimit.MaxLength); - assert.strictEqual(minKeywordStartCharFound, KeywordLimit.MinStartChar); - assert.strictEqual(maxKeywordStartCharFound, KeywordLimit.MaxStartChar); + assert.strictEqual( + minKeywordLengthFound, + KeywordLimit.MinLength, + `min keyword length is incorrect, set KeywordLimit.MinLength to ${minKeywordLengthFound}` + ); + assert.strictEqual( + maxKeywordLengthFound, + KeywordLimit.MaxLength, + `max keyword length is incorrect, set KeywordLimit.MaxLength to ${maxKeywordLengthFound}` + ); + assert.strictEqual( + minKeywordStartCharFound, + KeywordLimit.MinStartChar, + `min keyword start char is incorrect, set KeywordLimit.MinStartChar to ${minKeywordStartCharFound}` + ); + assert.strictEqual( + maxKeywordStartCharFound, + KeywordLimit.MaxStartChar, + `max keyword start char is incorrect, set KeywordLimit.MaxStartChar to ${maxKeywordStartCharFound}` + ); // check single character punctuation for (let i = 33; i <= 126; i++) { From 44faaadf014a05b190ee1289f7de07781e49bedb Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sun, 2 May 2021 12:14:10 -0700 Subject: [PATCH 17/47] More scanner optimization and fixes * Don't allocate substrings to match keywords. * Make at most one pass over string to get its unquoted/unescaped/unindented value. (**) * Add test coverage for impacted code paths. * Fix issue where string value of an unterminated string was missing final character(s) in string value. (**) Actually, we still make two passes in the case of a non-triple-quoted, multi-line string with \r\n, but that is about to be removed by the next commit which will disallow non-triple-quoted, multi-line strings altogether. --- packages/adl/compiler/charcode.ts | 24 +- packages/adl/compiler/scanner.ts | 407 +++++++++++++++++------------- packages/adl/test/test-parser.ts | 109 ++++++-- packages/adl/test/test-scanner.ts | 75 +++--- 4 files changed, 381 insertions(+), 234 deletions(-) diff --git a/packages/adl/compiler/charcode.ts b/packages/adl/compiler/charcode.ts index 94b6e7d13..d373b54fa 100644 --- a/packages/adl/compiler/charcode.ts +++ b/packages/adl/compiler/charcode.ts @@ -144,11 +144,15 @@ export const enum CharCode { Tilde = 0x7e, } +export function utf16CodeUnits(codePoint: number) { + return codePoint >= 0x10000 ? 2 : 1; +} + export function isAsciiLineBreak(ch: number) { return ch === CharCode.LineFeed || ch == CharCode.CarriageReturn; } -export function isAsciiWhiteSpaceSingleLine(ch: number): boolean { +export function isAsciiWhiteSpaceSingleLine(ch: number) { return ( ch === CharCode.Space || ch === CharCode.Tab || @@ -186,25 +190,29 @@ export function isWhiteSpaceSingleLine(ch: number) { ); } -export function isLineBreak(ch: number): boolean { +export function isLineBreak(ch: number) { return isAsciiLineBreak(ch) || (ch > CharCode.MaxAscii && isNonAsciiLineBreak(ch)); } -export function isDigit(ch: number): boolean { +export function isDigit(ch: number) { return ch >= CharCode._0 && ch <= CharCode._9; } -export function isHexDigit(ch: number): boolean { +export function isHexDigit(ch: number) { return ( isDigit(ch) || (ch >= CharCode.A && ch <= CharCode.F) || (ch >= CharCode.a && ch <= CharCode.f) ); } -export function isBinaryDigit(ch: number): boolean { +export function isBinaryDigit(ch: number) { return ch === CharCode._0 || ch === CharCode._1; } -export function isAsciiIdentifierStart(ch: number): boolean { +export function isLowercaseAsciiLetter(ch: number) { + return ch >= CharCode.a && ch <= CharCode.z; +} + +export function isAsciiIdentifierStart(ch: number) { return ( (ch >= CharCode.A && ch <= CharCode.Z) || (ch >= CharCode.a && ch <= CharCode.z) || @@ -213,7 +221,7 @@ export function isAsciiIdentifierStart(ch: number): boolean { ); } -export function isAsciiIdentifierContinue(ch: number): boolean { +export function isAsciiIdentifierContinue(ch: number) { return ( (ch >= CharCode.A && ch <= CharCode.Z) || (ch >= CharCode.a && ch <= CharCode.z) || @@ -245,7 +253,7 @@ export function isNonAsciiIdentifierContinue(codePoint: number) { return lookupInNonAsciiMap(codePoint, nonAsciiIdentifierContinueMap); } -function lookupInNonAsciiMap(codePoint: number, map: readonly number[]): boolean { +function lookupInNonAsciiMap(codePoint: number, map: readonly number[]) { // Bail out quickly if it couldn't possibly be in the map. if (codePoint < map[0]) { return false; diff --git a/packages/adl/compiler/scanner.ts b/packages/adl/compiler/scanner.ts index c2cfcaaed..16c8a34f4 100644 --- a/packages/adl/compiler/scanner.ts +++ b/packages/adl/compiler/scanner.ts @@ -7,11 +7,13 @@ import { isHexDigit, isIdentifierContinue, isLineBreak, + isLowercaseAsciiLetter, isNonAsciiIdentifierContinue, isNonAsciiIdentifierStart, isNonAsciiLineBreak, isNonAsciiWhiteSpaceSingleLine, isWhiteSpaceSingleLine, + utf16CodeUnits, } from "./charcode.js"; import { createSourceFile, Message, throwOnError } from "./diagnostics.js"; import { SourceFile } from "./types.js"; @@ -91,16 +93,16 @@ const MaxStatementKeyword = Token.AliasKeyword; /** @internal */ export const TokenDisplay: readonly string[] = [ - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", + "none", + "invalid", + "end of file", + "single-line comment", + "multi-line comment", + "newline", + "whitespace", + "conflict marker", + "numeric literal", + "string literal", "'{'", "'}'", "'('", @@ -119,7 +121,7 @@ export const TokenDisplay: readonly string[] = [ "'?'", "':'", "'@'", - "", + "identifier", "'import'", "'model'", "'namespace'", @@ -133,7 +135,7 @@ export const TokenDisplay: readonly string[] = [ ]; /** @internal */ -export const Keywords: ReadonlyMap = new Map([ +export const Keywords: readonly [string, Token][] = [ ["import", Token.ImportKeyword], ["model", Token.ModelKeyword], ["namespace", Token.NamespaceKeyword], @@ -144,14 +146,30 @@ export const Keywords: ReadonlyMap = new Map([ ["alias", Token.AliasKeyword], ["true", Token.TrueKeyword], ["false", Token.FalseKeyword], -]); +]; /** @internal */ export const enum KeywordLimit { MinLength = 2, + // If this ever exceeds 10, we will overflow the keyword map key, needing 11*5 + // = 55 bits or more, exceeding the JavaScript safe integer range. We would + // have to change the keyword lookup algorithm in that case. MaxLength = 9, - MinStartChar = CharCode.a, - MaxStartChar = CharCode.u, +} + +const KeywordMap: ReadonlyMap = new Map( + Keywords.map((e) => [keywordKey(e[0]), e[1]]) +); + +// Since keywords are short and all lowercase, we can pack the whole string into +// a single number by using 5 bits for each letter, and use that as the map key. +// This lets us lookup keywords without making temporary substrings. +function keywordKey(keyword: string) { + let key = 0; + for (let i = 0; i < keyword.length; i++) { + key = (key << 5) | (keyword.charCodeAt(i) - CharCode.a); + } + return key; } export interface Scanner { @@ -190,6 +208,7 @@ const enum TokenFlags { HasCrlf = 1 << 0, Escaped = 1 << 1, TripleQuoted = 1 << 2, + Unterminated = 1 << 3, } export function isLiteral(token: Token) { @@ -226,9 +245,8 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro const file = typeof source === "string" ? createSourceFile(source, "") : source; const input = file.text; let position = 0; - let token = Token.Invalid; + let token = Token.None; let tokenPosition = -1; - let tokenValue: string | undefined = undefined; let tokenFlags = TokenFlags.None; return { @@ -252,26 +270,20 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro return position >= input.length; } - function next(t: Token, count = 1) { - position += count; - return (token = t); - } - - function utf16CodeUnits(codePoint: number) { - return codePoint >= 0x10000 ? 2 : 1; - } - function getTokenText() { return input.substring(tokenPosition, position); } + function getTokenValue() { + return token === Token.StringLiteral ? getStringTokenValue() : getTokenText(); + } + function lookAhead(offset: number) { return input.charCodeAt(position + offset); } function scan(): Token { tokenPosition = position; - tokenValue = undefined; tokenFlags = TokenFlags.None; if (!eof()) { @@ -390,10 +402,14 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro : scanString(); default: - if (isAsciiIdentifierStart(ch)) { + if (isLowercaseAsciiLetter(ch)) { return scanIdentifierOrKeyword(); } + if (isAsciiIdentifierStart(ch)) { + return scanIdentifier(); + } + if (ch <= CharCode.MaxAscii) { return scanInvalidCharacter(); } @@ -405,6 +421,17 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro return (token = Token.EndOfFile); } + function next(t: Token, count = 1) { + position += count; + return (token = t); + } + + function unterminated(t: Token) { + tokenFlags |= TokenFlags.Unterminated; + error(Message.Unterminated, [TokenDisplay[t]]); + return (token = t); + } + function scanNonAsciiToken() { const ch = input.charCodeAt(position); @@ -416,9 +443,9 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro return scanWhitespace(); } - const codePoint = input.codePointAt(position)!; - if (isNonAsciiIdentifierStart(codePoint)) { - return scanNonAsciiIdentifierContinue(codePoint); + let cp = input.codePointAt(position)!; + if (isNonAsciiIdentifierStart(cp)) { + return scanNonAsciiIdentifier(cp); } return scanInvalidCharacter(); @@ -527,11 +554,10 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro function scanSingleLineComment() { position += 2; // consume '//' - while (!eof()) { + for (; !eof(); position++) { if (isLineBreak(input.charCodeAt(position))) { break; } - position++; } return (token = Token.SingleLineComment); @@ -540,22 +566,20 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro function scanMultiLineComment() { position += 2; // consume '/*' - while (!eof()) { + for (; !eof(); position++) { if (input.charCodeAt(position) === CharCode.Asterisk && lookAhead(1) === CharCode.Slash) { position += 2; return (token = Token.MultiLineComment); } - position++; } - error(Message.Unterminated, ["comment"]); - return (token = Token.MultiLineComment); + return unterminated(Token.MultiLineComment); } function scanString() { position++; // consume '"' - loop: while (!eof()) { + loop: for (; !eof(); position++) { const ch = input.charCodeAt(position); switch (ch) { case CharCode.CarriageReturn: @@ -570,150 +594,157 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro if (eof()) { break loop; } - break; + continue; case CharCode.DoubleQuote: position++; return (token = Token.StringLiteral); } - position++; } - error(Message.Unterminated, ["string literal"]); - return (token = Token.StringLiteral); + return unterminated(Token.StringLiteral); } function scanTripleQuotedString() { tokenFlags |= TokenFlags.TripleQuoted; position += 3; // consume '"""' - loop: while (!eof()) { - const ch = input.charCodeAt(position); - switch (ch) { - case CharCode.CarriageReturn: - if (lookAhead(1) === CharCode.LineFeed) { - tokenFlags |= TokenFlags.HasCrlf; - position++; - } - break; - case CharCode.Backslash: - tokenFlags |= TokenFlags.Escaped; - position++; - if (eof()) { - break loop; - } - break; - case CharCode.DoubleQuote: - if (lookAhead(1) === CharCode.DoubleQuote && lookAhead(2) === CharCode.DoubleQuote) { - position += 3; - return (token = Token.StringLiteral); - } - break; + for (; !eof(); position++) { + if ( + input.charCodeAt(position) === CharCode.DoubleQuote && + lookAhead(1) === CharCode.DoubleQuote && + lookAhead(2) === CharCode.DoubleQuote + ) { + position += 3; + return (token = Token.StringLiteral); } - position++; } - error(Message.Unterminated, ["string literal"]); - return (token = Token.StringLiteral); - } - - function getTokenValue() { - if (tokenValue !== undefined) { - return tokenValue; - } - return (tokenValue = token === Token.StringLiteral ? getStringTokenValue() : getTokenText()); + return unterminated(Token.StringLiteral); } function getStringTokenValue() { - // strip quotes const quoteLength = tokenFlags & TokenFlags.TripleQuoted ? 3 : 1; - let value = input.substring(tokenPosition + quoteLength, position - quoteLength); - - // Normalize CRLF to LF when interpreting value of multi-line string - // literals. Matches JavaScript behavior and ensures program behavior does - // not change due to line-ending conversion. - if (tokenFlags & TokenFlags.HasCrlf) { - value = value.replace(/\r\n/g, "\n"); - } + const start = tokenPosition + quoteLength; + const end = tokenFlags & TokenFlags.Unterminated ? position : position - quoteLength; if (tokenFlags & TokenFlags.TripleQuoted) { - value = unindentTripleQuoteString(value); + return unindentAndUnescapeTripleQuotedString(start, end); } if (tokenFlags & TokenFlags.Escaped) { - value = unescapeString(value); + return unescapeString(start, end); } - return (tokenValue = value); + let value = input.substring(start, end); + if (tokenFlags & TokenFlags.HasCrlf) { + value = value.replace(/\r\n/g, "\n"); + } + return value; } - function unindentTripleQuoteString(text: string) { - let start = 0; - let end = text.length; - + function unindentAndUnescapeTripleQuotedString(start: number, end: number) { // ignore leading whitespace before required initial line break - while (start < end && isWhiteSpaceSingleLine(text.charCodeAt(start))) { + while (start < end && isWhiteSpaceSingleLine(input.charCodeAt(start))) { start++; } // remove required initial line break - if (isLineBreak(text.charCodeAt(start))) { + if (isLineBreak(input.charCodeAt(start))) { + if (isCrlf(start, start, end)) { + start++; + } start++; } else { error(Message.NoNewLineAtStartOfTripleQuotedString); } - // remove whitespace before closing delimiter and record it as - // required indentation for all lines. - while (end > start && isWhiteSpaceSingleLine(text.charCodeAt(end - 1))) { + // remove whitespace before closing delimiter and record it as required + // indentation for all lines + const indentationEnd = end; + while (end > start && isWhiteSpaceSingleLine(input.charCodeAt(end - 1))) { end--; } - const indentation = text.substring(end, text.length); + const indentationStart = end; // remove required final line break - if (isLineBreak(text.charCodeAt(end - 1))) { + if (isLineBreak(input.charCodeAt(end - 1))) { + if (isCrlf(end - 2, start, end)) { + end--; + } end--; } else { error(Message.NoNewLineAtEndOfTripleQuotedString); } - // remove required matching indentation from each line - return removeMatchingIndentation(text, start, end, indentation); - } - - function removeMatchingIndentation( - text: string, - start: number, - end: number, - indentation: string - ) { + // remove required matching indentation from each line and unescape in the + // process of doing so let result = ""; let pos = start; - while (pos < end) { - start = skipMatchingIndentation(text, pos, end, indentation); - while (pos < end && !isLineBreak(text.charCodeAt(pos))) { - pos++; + // skip indentation at start of line + start = skipMatchingIndentation(pos, end, indentationStart, indentationEnd); + let ch; + + while (pos < end && !isLineBreak((ch = input.charCodeAt(pos)))) { + if (ch !== CharCode.Backslash) { + pos++; + continue; + } + result += input.substring(start, pos); + if (pos === end - 1) { + error(Message.InvalidEscapeSequence); + pos++; + } else { + result += unescapeOne(pos); + pos += 2; + } + start = pos; } + if (pos < end) { - pos++; // include line break + if (isCrlf(pos, start, end)) { + // CRLF in multi-line string is normalized to LF in string value. + // This keeps program behavior unchanged by line-eding conversion. + result += input.substring(start, pos); + result += "\n"; + pos += 2; + } else { + pos++; // include non-CRLF newline + result += input.substring(start, pos); + } + start = pos; } - result += text.substring(start, pos); } + result += input.substring(start, pos); return result; } - function skipMatchingIndentation(text: string, pos: number, end: number, indentation: string) { - end = Math.min(end, pos + indentation.length); + function isCrlf(pos: number, start: number, end: number) { + return ( + pos >= start && + pos < end - 1 && + input.charCodeAt(pos) === CharCode.CarriageReturn && + input.charCodeAt(pos + 1) === CharCode.LineFeed + ); + } + + function skipMatchingIndentation( + pos: number, + end: number, + indentationStart: number, + indentationEnd: number + ) { + let indentationPos = indentationStart; + end = Math.min(end, pos + (indentationEnd - indentationStart)); - let indentationPos = 0; while (pos < end) { - const ch = text.charCodeAt(pos); + const ch = input.charCodeAt(pos); if (isLineBreak(ch)) { // allow subset of indentation if line has only whitespace break; } - if (ch != indentation.charCodeAt(indentationPos)) { + if (ch !== input.charCodeAt(indentationPos)) { error(Message.InconsistentTripleQuoteIndentation); break; } @@ -724,76 +755,86 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro return pos; } - function unescapeString(text: string) { + function unescapeString(start: number, end: number) { let result = ""; - let start = 0; - let pos = 0; - const end = text.length; + let pos = start; while (pos < end) { - let ch = text.charCodeAt(pos); - if (ch != CharCode.Backslash) { + let ch = input.charCodeAt(pos); + if (ch !== CharCode.Backslash) { pos++; continue; } - result += text.substring(start, pos); - pos++; - ch = text.charCodeAt(pos); - - switch (ch) { - case CharCode.r: - result += "\r"; - break; - case CharCode.n: - result += "\n"; - break; - case CharCode.t: - result += "\t"; - break; - case CharCode.DoubleQuote: - result += '"'; - break; - case CharCode.Backslash: - result += "\\"; - break; - default: - error(Message.InvalidEscapeSequence); - result += String.fromCharCode(ch); - break; + if (pos === end - 1) { + error(Message.InvalidEscapeSequence); + break; } - pos++; + result += input.substring(start, pos); + result += unescapeOne(pos); + pos += 2; start = pos; } - result += text.substring(start, pos); + result += input.substring(start, pos); return result; } - function scanIdentifierOrKeyword() { - const startChar = input.charCodeAt(position); - let ch = startChar; - do { - position++; - } while (!eof() && isAsciiIdentifierContinue((ch = input.charCodeAt(position)))); + function unescapeOne(pos: number) { + const ch = input.charCodeAt(pos + 1); + switch (ch) { + case CharCode.r: + return "\r"; + case CharCode.n: + return "\n"; + case CharCode.t: + return "\t"; + case CharCode.DoubleQuote: + return '"'; + case CharCode.Backslash: + return "\\"; + default: + error(Message.InvalidEscapeSequence); + return String.fromCharCode(ch); + } + } - if (ch > CharCode.MaxAscii) { - const codePoint = input.codePointAt(position)!; - if (isNonAsciiIdentifierContinue(codePoint)) { - return scanNonAsciiIdentifierContinue(codePoint); + function scanIdentifierOrKeyword() { + let key = 0; + let count = 0; + let ch = input.charCodeAt(position); + + while (true) { + position++; + count++; + key = (key << 5) | (ch - CharCode.a); + + if (eof()) { + break; } + + ch = input.charCodeAt(position); + if (count < KeywordLimit.MaxLength && isLowercaseAsciiLetter(ch)) { + continue; + } + + if (isAsciiIdentifierContinue(ch)) { + return scanIdentifier(); + } + + if (ch > CharCode.MaxAscii) { + const cp = input.codePointAt(position)!; + if (isNonAsciiIdentifierContinue(cp)) { + return scanNonAsciiIdentifier(cp); + } + } + + break; } - const length = position - tokenPosition; - if ( - length >= KeywordLimit.MinLength && - length <= KeywordLimit.MaxLength && - startChar >= KeywordLimit.MinStartChar && - startChar <= KeywordLimit.MaxStartChar - ) { - tokenValue = getTokenText(); - const keyword = Keywords.get(tokenValue); + if (count >= KeywordLimit.MinLength && count <= KeywordLimit.MaxLength) { + const keyword = KeywordMap.get(key); if (keyword) { return (token = keyword); } @@ -802,11 +843,31 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro return (token = Token.Identifier); } - function scanNonAsciiIdentifierContinue(startCodePoint: number) { - let codePoint = startCodePoint; + function scanIdentifier() { + let ch: number; + do { - position += utf16CodeUnits(codePoint); - } while (!eof() && isIdentifierContinue((codePoint = input.codePointAt(position)!))); + position++; + if (eof()) { + return (token = Token.Identifier); + } + } while (isAsciiIdentifierContinue((ch = input.charCodeAt(position)))); + + if (ch > CharCode.MaxAscii) { + let cp = input.codePointAt(position)!; + if (isNonAsciiIdentifierContinue(cp)) { + return scanNonAsciiIdentifier(cp); + } + } + + return (token = Token.Identifier); + } + + function scanNonAsciiIdentifier(startCodePoint: number) { + let cp = startCodePoint; + do { + position += utf16CodeUnits(cp); + } while (!eof() && isIdentifierContinue((cp = input.codePointAt(position)!))); return (token = Token.Identifier); } diff --git a/packages/adl/test/test-parser.ts b/packages/adl/test/test-parser.ts index 5186d1a43..0dec9e7cf 100644 --- a/packages/adl/test/test-parser.ts +++ b/packages/adl/test/test-parser.ts @@ -1,4 +1,5 @@ import assert from "assert"; +import { CharCode } from "../compiler/charcode.js"; import { logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js"; import { hasParseError, NodeFlags, parse } from "../compiler/parser.js"; import { ADLScriptNode, SyntaxKind } from "../compiler/types.js"; @@ -223,16 +224,25 @@ describe("syntax", () => { }); describe("unterminated tokens", () => { - parseErrorEach([ - ['alias X = "banana', [/Unterminated string literal/]], - ['alias X = "banana\\', [/Unterminated string literal/]], - ['alias X = """\nbanana', [/Unterminated string literal/]], - ['alias X = """\nbanana\\', [/Unterminated string literal/]], - ["/* Yada yada yada", [/Unterminated comment/]], - ]); + parseErrorEach([["/* Yada yada yada", [/Unterminated multi-line comment/]]]); + + const strings = ['"banana', '"banana\\', '"""\nbanana', '"""\nbanana\\']; + parseErrorEach( + Array.from(strings.entries()).map((e) => [ + `alias ${String.fromCharCode(CharCode.A + e[0])} = ${e[1]}`, + [/Unterminated string literal/], + (node) => { + const statement = node.statements[0]; + assert(statement.kind === SyntaxKind.AliasStatement, "alias statement expected"); + const value = statement.value; + assert(value.kind === SyntaxKind.StringLiteral, "string literal expected"); + assert.strictEqual(value.value, "banana"); + }, + ]) + ); }); - describe("terminated tokens at EOF with missing semicolon", () => { + describe("terminated tokens at EOF", () => { parseErrorEach([ ["alias X = 0x10101", [/';' expected/]], ["alias X = 0xBEEF", [/';' expected/]], @@ -305,16 +315,68 @@ describe("syntax", () => { } }); - describe("non-ascii identifiers", () => { - parseEach([ - "model Incompréhensible {}", - "model 𐌰𐌲 {}", - "model Banana𐌰𐌲42Banana {}", - "model deaf\u{200c}ly {}", // ZWNJ - "model क्‍ष {}", // ZWJ - ]); - parseErrorEach([["model 😢 {}", [/Invalid character/]]]); + describe("identifiers", () => { + const good = [ + "short", + "short42", + "lowercaseandlong", + "lowercaseandlong42", + "camelCase", + "camelCase42", + "PascalCase", + "PascalCase42", + "has_underscore", + "has_$dollar", + "_startsWithUnderscore", + "$startsWithDollar", + "Incompréhensible", + "incompréhensible", + "IncomprÉhensible", + "incomprÉhensible", + // leading astral character + "𐌰𐌲", + // continuing astral character + "Banana𐌰𐌲42Banana", + "banana𐌰𐌲42banana", + // ZWNJ + "deaf\u{200c}ly", + // ZWJ + "क्‍ष", + ]; + + const bad: [string, RegExp][] = [ + ["😢", /Invalid character/], + ["42", /Identifier expected/], + ["true", /Keyword cannot be used as identifier/], + ]; + + parseEach( + good.map((s) => [ + `model ${s} {}`, + (node) => { + const statement = node.statements[0]; + assert(statement.kind === SyntaxKind.ModelStatement, "Model statement expected."); + assert.strictEqual(statement.id.sv, s); + }, + ]) + ); + + parseErrorEach(bad.map((e) => [`model ${e[0]} {}`, [e[1]]])); }); +}); + +// smaller repro of previous regen-samples baseline failures +describe("sample regressions", () => { + parseEach([ + [ + `/* \\n <-- before string! */ @format("\\\\w") model M {}`, + (node) => { + assert(node.statements[0].kind === SyntaxKind.ModelStatement); + assert(node.statements[0].decorators[0].arguments[0].kind === SyntaxKind.StringLiteral); + assert.strictEqual(node.statements[0].decorators[0].arguments[0].value, "\\w"); + }, + ], + ]); describe("enum statements", () => { parseEach([ @@ -344,7 +406,9 @@ describe("syntax", () => { }); }); -function parseEach(cases: (string | [string, (node: ADLScriptNode) => void])[]) { +type Callback = (node: ADLScriptNode) => void; + +function parseEach(cases: (string | [string, Callback])[]) { for (const each of cases) { const code = typeof each === "string" ? each : each[0]; const callback = typeof each === "string" ? undefined : each[1]; @@ -377,13 +441,16 @@ function parseEach(cases: (string | [string, (node: ADLScriptNode) => void])[]) } } -function parseErrorEach(cases: [string, RegExp[]][]) { - for (const [code, matches] of cases) { +function parseErrorEach(cases: [string, RegExp[], Callback?][], significantWhitespace = false) { + for (const [code, matches, callback] of cases) { it(`doesn't parse ${shorten(code)}`, () => { logVerboseTestOutput("=== Source ==="); logVerboseTestOutput(code); const astNode = parse(code); + if (callback) { + callback(astNode); + } logVerboseTestOutput("\n=== Parse Result ==="); dumpAST(astNode); @@ -404,7 +471,7 @@ function parseErrorEach(cases: [string, RegExp[]][]) { function dumpAST(astNode: ADLScriptNode) { logVerboseTestOutput((log) => { - const hasErrors = hasParseError(astNode); // force flags to initialize + hasParseError(astNode); // force flags to initialize const json = JSON.stringify(astNode, replacer, 2); log(json); }); diff --git a/packages/adl/test/test-scanner.ts b/packages/adl/test/test-scanner.ts index cb293de01..573e2e17b 100644 --- a/packages/adl/test/test-scanner.ts +++ b/packages/adl/test/test-scanner.ts @@ -2,7 +2,7 @@ import assert from "assert"; import { readFile } from "fs/promises"; import { URL } from "url"; import { isIdentifierContinue, isIdentifierStart } from "../compiler/charcode.js"; -import { throwOnError } from "../compiler/diagnostics.js"; +import { createDiagnostic, formatDiagnostic, throwOnError } from "../compiler/diagnostics.js"; import { createScanner, isKeyword, @@ -180,11 +180,21 @@ describe("scanner", () => { ]); }); - function scanString(text: string, expectedValue: string) { - const scanner = createScanner(text); + function scanString(text: string, expectedValue: string, expectedDiagnostic?: RegExp) { + const scanner = createScanner(text, (message, target, args) => { + const diagnostic = createDiagnostic(message, target, args); + if (expectedDiagnostic) { + assert.match(diagnostic.message, expectedDiagnostic); + } else { + assert.fail("No diagnostic expected, but got " + formatDiagnostic(diagnostic)); + } + }); + assert.strictEqual(scanner.scan(), Token.StringLiteral); assert.strictEqual(scanner.token, Token.StringLiteral); - assert.strictEqual(scanner.getTokenText(), text); + if (!expectedDiagnostic) { + assert.strictEqual(scanner.getTokenText(), text); + } assert.strictEqual(scanner.getTokenValue(), expectedValue); } @@ -202,19 +212,24 @@ describe("scanner", () => { it("scans triple-quoted strings", () => { scanString( + // NOTE: sloppy blank line formatting and trailing whitespace after open + // quotes above is deliberate here and deliberately tolerated by + // the scanner. `""" This is a triple-quoted string - - And this is another line + "You do not need to escape lone quotes" + You can use escape sequences: \\r \\n \\t \\\\ \\" """`, - // NOTE: sloppy blank line formatting and trailing whitespace after open - // quotes above is deliberately tolerated. - "This is a triple-quoted string\n\n\n\nAnd this is another line" + 'This is a triple-quoted string\n\n\n"You do not need to escape lone quotes"\nYou can use escape sequences: \r \n \t \\ "' ); }); + it("normalizes CRLF to LF in multi-line string", () => { + scanString('"""\r\nThis\r\nis\r\na\r\ntest\r\n"""', "This\nis\na\ntest"); + }); + it("provides token position", () => { const all = tokens("a x\raa x\r\naaa x\naaaa x\u{2028}aaaaa x\u{2029}aaaaaa x"); verify(all, [ @@ -263,14 +278,15 @@ describe("scanner", () => { const nonStatementKeywords = [Token.ExtendsKeyword, Token.TrueKeyword, Token.FalseKeyword]; let minKeywordLengthFound = Number.MAX_SAFE_INTEGER; let maxKeywordLengthFound = Number.MIN_SAFE_INTEGER; - let minKeywordStartCharFound = Number.MAX_SAFE_INTEGER; - let maxKeywordStartCharFound = Number.MIN_SAFE_INTEGER; - for (const [name, token] of Keywords.entries()) { + for (const [name, token] of Keywords) { + assert.match( + name, + /^[a-z]+$/, + "We need to change the keyword lookup algorithm in the scanner if we ever add a keyword that is not all lowercase ascii letters." + ); minKeywordLengthFound = Math.min(minKeywordLengthFound, name.length); maxKeywordLengthFound = Math.max(maxKeywordLengthFound, name.length); - minKeywordStartCharFound = Math.min(minKeywordStartCharFound, name.charCodeAt(0)); - maxKeywordStartCharFound = Math.max(maxKeywordStartCharFound, name.charCodeAt(0)); assert.strictEqual(TokenDisplay[token], `'${name}'`); assert(isKeyword(token), `${name} should be classified as a keyword`); @@ -289,15 +305,10 @@ describe("scanner", () => { KeywordLimit.MaxLength, `max keyword length is incorrect, set KeywordLimit.MaxLength to ${maxKeywordLengthFound}` ); - assert.strictEqual( - minKeywordStartCharFound, - KeywordLimit.MinStartChar, - `min keyword start char is incorrect, set KeywordLimit.MinStartChar to ${minKeywordStartCharFound}` - ); - assert.strictEqual( - maxKeywordStartCharFound, - KeywordLimit.MaxStartChar, - `max keyword start char is incorrect, set KeywordLimit.MaxStartChar to ${maxKeywordStartCharFound}` + + assert( + maxKeywordLengthFound < 11, + "We need to change the keyword lookup algorithm in the scanner if we ever add a keyword with 11 characters or more." ); // check single character punctuation @@ -317,15 +328,15 @@ describe("scanner", () => { // check the rest assert.strictEqual(TokenDisplay[Token.Elipsis], "'...'"); - assert.strictEqual(TokenDisplay[Token.None], ""); - assert.strictEqual(TokenDisplay[Token.Invalid], ""); - assert.strictEqual(TokenDisplay[Token.EndOfFile], ""); - assert.strictEqual(TokenDisplay[Token.SingleLineComment], ""); - assert.strictEqual(TokenDisplay[Token.MultiLineComment], ""); - assert.strictEqual(TokenDisplay[Token.NewLine], ""); - assert.strictEqual(TokenDisplay[Token.Whitespace], ""); - assert.strictEqual(TokenDisplay[Token.ConflictMarker], ""); - assert.strictEqual(TokenDisplay[Token.Identifier], ""); + assert.strictEqual(TokenDisplay[Token.None], "none"); + assert.strictEqual(TokenDisplay[Token.Invalid], "invalid"); + assert.strictEqual(TokenDisplay[Token.EndOfFile], "end of file"); + assert.strictEqual(TokenDisplay[Token.SingleLineComment], "single-line comment"); + assert.strictEqual(TokenDisplay[Token.MultiLineComment], "multi-line comment"); + assert.strictEqual(TokenDisplay[Token.NewLine], "newline"); + assert.strictEqual(TokenDisplay[Token.Whitespace], "whitespace"); + assert.strictEqual(TokenDisplay[Token.ConflictMarker], "conflict marker"); + assert.strictEqual(TokenDisplay[Token.Identifier], "identifier"); }); // Search for Other_ID_Start in https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt From 7c6ef01053ea2a682b758f95867fe83a54de49fb Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Sun, 2 May 2021 12:33:43 -0700 Subject: [PATCH 18/47] Do not allow non-triple-quoted strings to be multi-line --- packages/adl-language/src/spec.emu.html | 12 +++++++-- packages/adl-vscode/src/tmlanguage.ts | 34 +++++++++++++++++++++---- packages/adl/compiler/scanner.ts | 27 +++++++++----------- packages/adl/test/test-parser.ts | 12 ++++++++- packages/adl/test/test-scanner.ts | 7 +++-- 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/packages/adl-language/src/spec.emu.html b/packages/adl-language/src/spec.emu.html index 2d8729a90..bc304fb30 100644 --- a/packages/adl-language/src/spec.emu.html +++ b/packages/adl-language/src/spec.emu.html @@ -111,15 +111,23 @@ BinaryDigits : BinaryDigit : one of `0` `1` -// TODO: triple-quoted strings not specified yet, tricky to express. - +// NOTE: This does not specify the extra rules about '"""'s going +// on their own lines and having consistent indentation. StringLiteral : `"` StringCharacters? `"` + `"""` TripleQuotedStringCharacters? `"""` StringCharacters : StringCharacter StringCharacters? StringCharacter : + SourceCharacter but not one of `"` or `\` or LineTerminator + `\` EscapeCharacter + +TripleQuotedStringCharacters + TripleQuotedStringCharacter TripleQuotedStringCharacters? + +TripleQuotedStringCharacter : SourceCharacter but not one of `"` or `\` `\` EscapeCharacter diff --git a/packages/adl-vscode/src/tmlanguage.ts b/packages/adl-vscode/src/tmlanguage.ts index 10b5f1c76..620641329 100644 --- a/packages/adl-vscode/src/tmlanguage.ts +++ b/packages/adl-vscode/src/tmlanguage.ts @@ -21,6 +21,7 @@ type ADLScope = | "entity.name.function.adl" | "keyword.other.adl" | "string.quoted.double.adl" + | "string.quoted.triple.adl" | "variable.name.adl"; const meta: typeof tm.meta = tm.meta; @@ -72,13 +73,19 @@ const escapeChar: MatchRule = { match: "\\\\.", }; -// TODO: Triple-quoted """X""" currently matches as three string literals -// ("" "X" "") but should be its own thing. const stringLiteral: BeginEndRule = { key: "string-literal", scope: "string.quoted.double.adl", begin: '"', - end: '"', + end: '"|$', + patterns: [escapeChar], +}; + +const tripleQuotedStringLiteral: BeginEndRule = { + key: "triple-quoted-string-literal", + scope: "string.quoted.triple.adl", + begin: '"""', + end: '"""', patterns: [escapeChar], }; @@ -104,7 +111,16 @@ const blockComment: BeginEndRule = { // Tokens that match standing alone in any context: literals and comments const token: IncludeRule = { key: "token", - patterns: [lineComment, blockComment, stringLiteral, booleanLiteral, numericLiteral], + patterns: [ + lineComment, + blockComment, + // `"""` must come before `"` or first two quotes of `"""` will match as + // empty string + tripleQuotedStringLiteral, + stringLiteral, + booleanLiteral, + numericLiteral, + ], }; const parenthesizedExpression: BeginEndRule = { @@ -181,7 +197,15 @@ const modelExpression: BeginEndRule = { scope: meta, begin: "\\{", end: "\\}", - patterns: [token, decorator, modelProperty, modelSpreadProperty], + patterns: [ + // modelProperty must come before token or quoted property name will be + // considered an arbitrarily positioned string literal and not match as part + // of modelProperty begin. + modelProperty, + token, + decorator, + modelSpreadProperty, + ], }; const modelHeritage: BeginEndRule = { diff --git a/packages/adl/compiler/scanner.ts b/packages/adl/compiler/scanner.ts index 16c8a34f4..fb08d692a 100644 --- a/packages/adl/compiler/scanner.ts +++ b/packages/adl/compiler/scanner.ts @@ -205,10 +205,9 @@ export interface Scanner { const enum TokenFlags { None = 0, - HasCrlf = 1 << 0, - Escaped = 1 << 1, - TripleQuoted = 1 << 2, - Unterminated = 1 << 3, + Escaped = 1 << 0, + TripleQuoted = 1 << 1, + Unterminated = 1 << 2, } export function isLiteral(token: Token) { @@ -582,12 +581,6 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro loop: for (; !eof(); position++) { const ch = input.charCodeAt(position); switch (ch) { - case CharCode.CarriageReturn: - if (lookAhead(1) === CharCode.LineFeed) { - tokenFlags |= TokenFlags.HasCrlf; - position++; - } - break; case CharCode.Backslash: tokenFlags |= TokenFlags.Escaped; position++; @@ -598,6 +591,14 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro case CharCode.DoubleQuote: position++; return (token = Token.StringLiteral); + case CharCode.CarriageReturn: + case CharCode.LineFeed: + break loop; + default: + if (ch > CharCode.MaxAscii && isNonAsciiLineBreak(ch)) { + break loop; + } + continue; } } @@ -635,11 +636,7 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro return unescapeString(start, end); } - let value = input.substring(start, end); - if (tokenFlags & TokenFlags.HasCrlf) { - value = value.replace(/\r\n/g, "\n"); - } - return value; + return input.substring(start, end); } function unindentAndUnescapeTripleQuotedString(start: number, end: number) { diff --git a/packages/adl/test/test-parser.ts b/packages/adl/test/test-parser.ts index 0dec9e7cf..3e1b83919 100644 --- a/packages/adl/test/test-parser.ts +++ b/packages/adl/test/test-parser.ts @@ -226,7 +226,17 @@ describe("syntax", () => { describe("unterminated tokens", () => { parseErrorEach([["/* Yada yada yada", [/Unterminated multi-line comment/]]]); - const strings = ['"banana', '"banana\\', '"""\nbanana', '"""\nbanana\\']; + const strings = [ + '"banana', + '"banana\\', + '"banana\r"', + '"banana\n"', + '"banana\r\n"', + '"banana\u{2028}"', + '"banana\u{2029}"', + '"""\nbanana', + '"""\nbanana\\', + ]; parseErrorEach( Array.from(strings.entries()).map((e) => [ `alias ${String.fromCharCode(CharCode.A + e[0])} = ${e[1]}`, diff --git a/packages/adl/test/test-scanner.ts b/packages/adl/test/test-scanner.ts index 573e2e17b..bf517c82b 100644 --- a/packages/adl/test/test-scanner.ts +++ b/packages/adl/test/test-scanner.ts @@ -206,8 +206,11 @@ describe("scanner", () => { scanString('"Hello world \\r\\n \\t \\" \\\\ !"', 'Hello world \r\n \t " \\ !'); }); - it("scans multi-line strings", () => { - scanString('"More\r\nthan\r\none\r\nline"', "More\nthan\none\nline"); + it("does not allow multi-line, non-triple-quoted strings", () => { + scanString('"More\r\nthan\r\none\r\nline"', "More", /Unterminated string/); + scanString('"More\nthan\none\nline"', "More", /Unterminated string/); + scanString('"Fancy\u{2028}line separator"', "Fancy", /Unterminated string/); + scanString('"Fancy\u{2029}paragraph separator', "Fancy", /Unterminated string/); }); it("scans triple-quoted strings", () => { From 932c8bdc31b0e1dd30ef2c08efdcac9379eba90f Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Mon, 3 May 2021 12:51:18 -0700 Subject: [PATCH 19/47] Disallow mocha .only in CI and PR validation (#516) --- packages/adl/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/adl/package.json b/packages/adl/package.json index 85d3e5db8..78bf6d994 100644 --- a/packages/adl/package.json +++ b/packages/adl/package.json @@ -38,6 +38,7 @@ "watch": "tsc -p . --watch", "dogfood": "node scripts/dogfood.js", "test": "mocha --timeout 5000 --require source-map-support/register --ignore 'dist/test/manual/**/*.js' 'dist/test/**/*.js'", + "test-official": "mocha --forbid-only --timeout 5000 --require source-map-support/register --ignore 'dist/test/manual/**/*.js' 'dist/test/**/*.js'", "regen-samples": "node scripts/regen-samples.js", "regen-nonascii": "node scripts/regen-nonascii.js", "fuzz": "node dist/test/manual/fuzz.js run" From d4924d88a636cb6cc8d513a44741c592704f28cf Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Mon, 3 May 2021 14:59:30 -0700 Subject: [PATCH 20/47] Add describe prefix for better test navigation in IDE (#518) Also fix a mistaken test grouping due to incorrect merge conflict resolution --- packages/adl/test/checker/alias.ts | 2 +- packages/adl/test/checker/duplicate-ids.ts | 2 +- packages/adl/test/checker/enum.ts | 2 +- packages/adl/test/checker/loader.ts | 2 +- packages/adl/test/checker/namespaces.ts | 4 +-- packages/adl/test/checker/spread.ts | 2 +- packages/adl/test/checker/using.ts | 2 +- packages/adl/test/decorators/range-limits.ts | 2 +- packages/adl/test/libraries/test-libraries.ts | 2 +- packages/adl/test/test-parser.ts | 28 +++++++++---------- packages/adl/test/test-scanner.ts | 2 +- 11 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/adl/test/checker/alias.ts b/packages/adl/test/checker/alias.ts index 73aedb865..4497d5c07 100644 --- a/packages/adl/test/checker/alias.ts +++ b/packages/adl/test/checker/alias.ts @@ -2,7 +2,7 @@ import { ok, strictEqual } from "assert"; import { ModelType, UnionType } from "../../compiler/types.js"; import { createTestHost, TestHost } from "../test-host.js"; -describe("aliases", () => { +describe("adl: aliases", () => { let testHost: TestHost; beforeEach(async () => { diff --git a/packages/adl/test/checker/duplicate-ids.ts b/packages/adl/test/checker/duplicate-ids.ts index 33ae8f0ed..10e70dc3c 100644 --- a/packages/adl/test/checker/duplicate-ids.ts +++ b/packages/adl/test/checker/duplicate-ids.ts @@ -1,7 +1,7 @@ import { rejects } from "assert"; import { createTestHost, TestHost } from "../test-host.js"; -describe("duplicate declarations", () => { +describe("adl: duplicate declarations", () => { let testHost: TestHost; beforeEach(async () => { diff --git a/packages/adl/test/checker/enum.ts b/packages/adl/test/checker/enum.ts index db2fd819e..a2b782163 100644 --- a/packages/adl/test/checker/enum.ts +++ b/packages/adl/test/checker/enum.ts @@ -2,7 +2,7 @@ import { ok, strictEqual } from "assert"; import { EnumMemberType, EnumType, ModelType } from "../../compiler/types.js"; import { createTestHost, TestHost } from "../test-host.js"; -describe("enums", () => { +describe("adl: enums", () => { let testHost: TestHost; beforeEach(async () => { diff --git a/packages/adl/test/checker/loader.ts b/packages/adl/test/checker/loader.ts index 3606dc6c8..893aa54fd 100644 --- a/packages/adl/test/checker/loader.ts +++ b/packages/adl/test/checker/loader.ts @@ -1,6 +1,6 @@ import { createTestHost, TestHost } from "../test-host.js"; -describe("loader", () => { +describe("adl: loader", () => { let testHost: TestHost; beforeEach(async () => { diff --git a/packages/adl/test/checker/namespaces.ts b/packages/adl/test/checker/namespaces.ts index 2f4a6e4db..3ff941ef2 100644 --- a/packages/adl/test/checker/namespaces.ts +++ b/packages/adl/test/checker/namespaces.ts @@ -2,7 +2,7 @@ import { ok, strictEqual } from "assert"; import { ModelType, NamespaceType, Type } from "../../compiler/types.js"; import { createTestHost, TestHost } from "../test-host.js"; -describe("namespaces with blocks", () => { +describe("adl: namespaces with blocks", () => { const blues = new WeakSet(); function blue(_: any, target: Type) { blues.add(target); @@ -152,7 +152,7 @@ describe("namespaces with blocks", () => { }); }); -describe("blockless namespaces", () => { +describe("adl: blockless namespaces", () => { const blues = new WeakSet(); function blue(_: any, target: Type) { blues.add(target); diff --git a/packages/adl/test/checker/spread.ts b/packages/adl/test/checker/spread.ts index 83991a1ef..8243d9cc3 100644 --- a/packages/adl/test/checker/spread.ts +++ b/packages/adl/test/checker/spread.ts @@ -2,7 +2,7 @@ import { ok, strictEqual } from "assert"; import { ModelType, Type } from "../../compiler/types.js"; import { createTestHost, TestHost } from "../test-host.js"; -describe("spread", () => { +describe("adl: spread", () => { const blues = new WeakSet(); function blue(_: any, target: Type) { blues.add(target); diff --git a/packages/adl/test/checker/using.ts b/packages/adl/test/checker/using.ts index 0bab618a2..fc5d56af3 100644 --- a/packages/adl/test/checker/using.ts +++ b/packages/adl/test/checker/using.ts @@ -2,7 +2,7 @@ import { rejects, strictEqual } from "assert"; import { ModelType } from "../../compiler/types"; import { createTestHost, TestHost } from "../test-host.js"; -describe("using statements", () => { +describe("adl: using statements", () => { let testHost: TestHost; beforeEach(async () => { diff --git a/packages/adl/test/decorators/range-limits.ts b/packages/adl/test/decorators/range-limits.ts index 493f2aed7..d2253aba4 100644 --- a/packages/adl/test/decorators/range-limits.ts +++ b/packages/adl/test/decorators/range-limits.ts @@ -3,7 +3,7 @@ import { ModelType } from "../../compiler/types.js"; import { getMaxValue, getMinValue } from "../../lib/decorators.js"; import { createTestHost, TestHost } from "../test-host.js"; -describe("range limiting decorators", () => { +describe("adl: range limiting decorators", () => { let testHost: TestHost; beforeEach(async () => { diff --git a/packages/adl/test/libraries/test-libraries.ts b/packages/adl/test/libraries/test-libraries.ts index 09ea91dae..e82b04bd0 100644 --- a/packages/adl/test/libraries/test-libraries.ts +++ b/packages/adl/test/libraries/test-libraries.ts @@ -4,7 +4,7 @@ import { NodeHost } from "../../compiler/util.js"; const libs = ["simple"]; -describe("libraries", () => { +describe("adl: libraries", () => { for (const lib of libs) { describe(lib, () => { it("compiles without error", async () => { diff --git a/packages/adl/test/test-parser.ts b/packages/adl/test/test-parser.ts index 3e1b83919..d0cb09577 100644 --- a/packages/adl/test/test-parser.ts +++ b/packages/adl/test/test-parser.ts @@ -4,7 +4,7 @@ import { logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js import { hasParseError, NodeFlags, parse } from "../compiler/parser.js"; import { ADLScriptNode, SyntaxKind } from "../compiler/types.js"; -describe("syntax", () => { +describe("adl: syntax", () => { describe("import statements", () => { parseEach(['import "x";']); @@ -373,20 +373,20 @@ describe("syntax", () => { parseErrorEach(bad.map((e) => [`model ${e[0]} {}`, [e[1]]])); }); -}); -// smaller repro of previous regen-samples baseline failures -describe("sample regressions", () => { - parseEach([ - [ - `/* \\n <-- before string! */ @format("\\\\w") model M {}`, - (node) => { - assert(node.statements[0].kind === SyntaxKind.ModelStatement); - assert(node.statements[0].decorators[0].arguments[0].kind === SyntaxKind.StringLiteral); - assert.strictEqual(node.statements[0].decorators[0].arguments[0].value, "\\w"); - }, - ], - ]); + // smaller repro of previous regen-samples baseline failures + describe("sample regressions", () => { + parseEach([ + [ + `/* \\n <-- before string! */ @format("\\\\w") model M {}`, + (node) => { + assert(node.statements[0].kind === SyntaxKind.ModelStatement); + assert(node.statements[0].decorators[0].arguments[0].kind === SyntaxKind.StringLiteral); + assert.strictEqual(node.statements[0].decorators[0].arguments[0].value, "\\w"); + }, + ], + ]); + }); describe("enum statements", () => { parseEach([ diff --git a/packages/adl/test/test-scanner.ts b/packages/adl/test/test-scanner.ts index bf517c82b..69c661f60 100644 --- a/packages/adl/test/test-scanner.ts +++ b/packages/adl/test/test-scanner.ts @@ -92,7 +92,7 @@ function verify(tokens: TokenEntry[], expecting: TokenEntry[]) { } } -describe("scanner", () => { +describe("adl: scanner", () => { /** verifies that we can scan tokens and get back some output. */ it("smoketest", () => { const all = tokens('\tthis is "a" test'); From efe9479cd4fea15445067085ed623df705f77eac Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Mon, 3 May 2021 15:39:09 -0700 Subject: [PATCH 21/47] Clean up editor settings (#519) 1. Move to per-language prettier formatter throughout. Global wasn't working for some folks for TypeScript, so I suspect it's best to use the per-language setting throughout. 2. Merge the adl-vs .editorconfig into the root .editorconfig, giving better defaults for editors other than VS Code that support .editorconfig. 3. Add ADL, C# and XML to VS Code settings.json --- packages/adl-vs/.editorconfig | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 packages/adl-vs/.editorconfig diff --git a/packages/adl-vs/.editorconfig b/packages/adl-vs/.editorconfig deleted file mode 100644 index 4819a04a7..000000000 --- a/packages/adl-vs/.editorconfig +++ /dev/null @@ -1,16 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -charset = utf-8 - -[*.cs] -csharp_new_line_before_open_brace = none -csharp_new_line_before_catch = false -csharp_new_line_before_else = false -csharp_new_line_before_finally = false -csharp_new_line_before_members_in_anonymous_types = false -csharp_new_line_before_members_in_object_initializers = false From d1401ef0650adb30b45b81e26470b2d03e73fa32 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Mon, 3 May 2021 16:56:45 -0700 Subject: [PATCH 22/47] Don't warn on developer builds without VS (#521) The warning was breaking `rush dogfood` --- packages/adl-vs/scripts/build.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/adl-vs/scripts/build.js b/packages/adl-vs/scripts/build.js index 0d7fcdee9..9750a3866 100644 --- a/packages/adl-vs/scripts/build.js +++ b/packages/adl-vs/scripts/build.js @@ -40,13 +40,8 @@ if (proc.status != 0 || proc.error || !proc.stdout) { // In official build on Windows, it's an error if VS is not found. console.error(`error: ${message}`); process.exit(1); - } else if (proc.error?.code === "ENOENT") { - // If developer has no version of VS installed, skip build without warning. - console.log(`Skipping adl-vs build: ${message}.`); - process.exit(0); } else { - // If developer has VS but it's not recent enough, skip build with warning. - console.error(`warning: ${message}. Skipping adl-vs build.`); + console.log(`Skipping adl-vs build: ${message}.`); process.exit(0); } } From 9d3e732f7831e829b73c0cb790857ebcf84aeab3 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Tue, 4 May 2021 15:07:35 -0700 Subject: [PATCH 23/47] Update tutorial with language changes (#535) --- docs/tutorial.md | 50 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index c781a8301..fdeda6e7e 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -96,6 +96,34 @@ model Pet { model Dog extends Pet, Animal { } ``` +### Enums + +Enums define a type which can hold one of a set of constant values. + +``` +enum Color { + Red, + Blue, + Green, +} +``` + +In this case, we haven't specified how the constants will be represented, allowing for different choices in different scenarios. For example, the OpenAPI emitter will choose string values "Red", "Green", "Blue". Another protocol might prefer to assign incrementing numeric values 0, 1, 2. + +We can also specify explicit string or numeric values: +``` +enum Color { + Red: "red", + Blue: "blue" + Green: "green", +} + +enum Priority { + High: 100, + Low: 0, +} +``` + #### Templates It is often useful to let the users of a model fill in certain details. Model templates enable this pattern. Similar to generics found in other languages, model templates declare template parameters that users provide when referencing the model. @@ -111,14 +139,16 @@ model DogPage { } ``` -#### Model Aliases +#### Type Aliases -Sometimes it's convenient to alias a model template instantiation or model produced via type operators (covered later) as a convenient name. Model aliases allow this: +Sometimes it's convenient to alias a model template instantiation or type produced via type operators (covered later) as a convenient name. Aliases allow this: ``` -model DogPage = Page; +alias DogPage = Page; ``` +Unlike `model`, `alias` does not create a new entity, and as such will not change generated code in any way. An alias merely describes a source code shorthand to avoid repeating the right-hand side in multiple places. + ### Type Literals API authors often need to describe API shapes in terms of specific literal values. For example, this operation returns this specific integer status code, or this model member can be one of a few specific string values. It is also often useful to pass specific literal values to decorators. ADL supports string, number, and boolean literal values to support these cases: @@ -153,7 +183,7 @@ ADL supports a few type operators that make it easy to compose new models from o Unions describe a type that must be exactly one of the union's constituents. Create a union with the `|` operator. ``` -model GoodBreeds = 'Beagle' | 'German Shepherd' | 'Golden Retriever'; +alias GoodBreed = Beagle | GermanShepherd | GoldenRetriever; ``` #### Intersection @@ -161,7 +191,7 @@ model GoodBreeds = 'Beagle' | 'German Shepherd' | 'Golden Retriever'; Intersections describe a type that must include all of the intersection's constituents. Create an intersection with the `&` operator. ``` -model Dog = Animal & Pet; +alias Dog = Animal & Pet; ``` #### Array @@ -169,7 +199,7 @@ model Dog = Animal & Pet; Arrays describe lists of things. Create an Array type with the `[]` operator. ``` -model Pack = Dog[]; +alias Pack = Dog[]; ``` ### Operations @@ -228,7 +258,7 @@ namespace A.B; namespace C.D {} namespace C.D.E { model M { }} -model M = A.B.C.D.E.M; +alias M = A.B.C.D.E.M; ``` It can be convenient to add references to a namespace's declarations to your local namespace, especially when namespaces can become deeply nested. The `using` statement lets us do this: @@ -253,11 +283,11 @@ namespace Test { namespace Test2 { using Test; - model B = A; // ok + alias B = A; // ok } -model C = Test2.A; // not ok -model C = Test2.B; // ok +alias C = Test2.A; // not ok +alias C = Test2.B; // ok ``` ### Imports From 261465f0dd38a29b44683a8d4e4060f4e15fe972 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 5 May 2021 13:11:35 -0700 Subject: [PATCH 24/47] Make blockless namespaces accumulate declarations. (#543) --- packages/adl/compiler/checker.ts | 37 ++++++++++------------ packages/adl/test/checker/namespaces.ts | 41 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/packages/adl/compiler/checker.ts b/packages/adl/compiler/checker.ts index 9066ce2e8..f979ed9c2 100644 --- a/packages/adl/compiler/checker.ts +++ b/packages/adl/compiler/checker.ts @@ -413,19 +413,7 @@ export function createChecker(program: Program) { } if (Array.isArray(node.statements)) { - for (const statement of node.statements.map(getTypeForNode)) { - switch (statement.kind) { - case "Model": - type.models.set(statement.name, statement); - break; - case "Operation": - type.operations.set(statement.name, statement); - break; - case "Namespace": - type.namespaces.set(statement.name, statement); - break; - } - } + node.statements.forEach(getTypeForNode); } else if (node.statements) { const subNs = checkNamespace(node.statements); type.namespaces.set(subNs.name, subNs); @@ -439,16 +427,18 @@ export function createChecker(program: Program) { const symbolLinks = getSymbolLinks(node.symbol); if (!symbolLinks.type) { // haven't seen this namespace before + const namespace = getParentNamespaceType(node); + const name = node.name.sv; const type: NamespaceType = createType({ kind: "Namespace", - name: node.name.sv, - namespace: getParentNamespaceType(node), - node: node, + name, + namespace, + node, models: new Map(), operations: new Map(), namespaces: new Map(), }); - + namespace?.namespaces.set(name, type); symbolLinks.type = type; } else { // seen it before, need to execute the decorators on this node @@ -472,14 +462,18 @@ export function createChecker(program: Program) { } function checkOperation(node: OperationStatementNode): OperationType { - return createType({ + const namespace = getParentNamespaceType(node); + const name = node.id.sv; + const type = createType({ kind: "Operation", - name: node.id.sv, - namespace: getParentNamespaceType(node), - node: node, + name, + namespace, + node, parameters: getTypeForNode(node.parameters) as ModelType, returnType: getTypeForNode(node.returnType), }); + namespace?.operations.set(name, type); + return type; } function checkTupleExpression(node: TupleExpressionNode): TupleType { @@ -660,6 +654,7 @@ export function createChecker(program: Program) { if (!instantiatingThisTemplate) { links.declaredType = type; links.instantiations = new TypeInstantiationMap(); + type.namespace?.models.set(type.name, type); } // The model is fully created now diff --git a/packages/adl/test/checker/namespaces.ts b/packages/adl/test/checker/namespaces.ts index 3ff941ef2..f744d84a6 100644 --- a/packages/adl/test/checker/namespaces.ts +++ b/packages/adl/test/checker/namespaces.ts @@ -150,6 +150,27 @@ describe("adl: namespaces with blocks", () => { ); await testHost.compile("./"); }); + + it("accumulates declarations inside of it", async () => { + testHost.addAdlFile( + "a.adl", + ` + @test namespace Foo { + namespace Bar { }; + op Baz(): {}; + model Qux { }; + } + ` + ); + + const { Foo } = (await testHost.compile("/a.adl")) as { + Foo: NamespaceType; + }; + + strictEqual(Foo.operations.size, 1); + strictEqual(Foo.models.size, 1); + strictEqual(Foo.namespaces.size, 1); + }); }); describe("adl: blockless namespaces", () => { @@ -339,4 +360,24 @@ describe("adl: blockless namespaces", () => { await testHost.compile("/a.adl"); }); + + it("accumulates declarations inside of it", async () => { + testHost.addAdlFile( + "a.adl", + ` + @test namespace Foo; + namespace Bar { }; + op Baz(): {}; + model Qux { }; + ` + ); + + const { Foo } = (await testHost.compile("/a.adl")) as { + Foo: NamespaceType; + }; + + strictEqual(Foo.operations.size, 1); + strictEqual(Foo.models.size, 1); + strictEqual(Foo.namespaces.size, 1); + }); }); From 3f58a55bc33197581693231d08033fb87307a34d Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 6 May 2021 08:27:14 -0700 Subject: [PATCH 25/47] Prepare publish of adl 0.10.0 (#544) --- packages/adl-rest/CHANGELOG.json | 25 +++++++++++++++++ packages/adl-rest/CHANGELOG.md | 14 +++++++++- packages/adl-rest/package.json | 4 +-- packages/adl-vs/CHANGELOG.json | 12 ++++++++ packages/adl-vs/CHANGELOG.md | 7 ++++- packages/adl-vs/package.json | 4 +-- packages/adl-vscode/CHANGELOG.json | 17 ++++++++++++ packages/adl-vscode/CHANGELOG.md | 13 ++++++++- packages/adl-vscode/package.json | 2 +- packages/adl/CHANGELOG.json | 44 ++++++++++++++++++++++++++++++ packages/adl/CHANGELOG.md | 22 ++++++++++++++- packages/adl/package.json | 2 +- 12 files changed, 156 insertions(+), 10 deletions(-) diff --git a/packages/adl-rest/CHANGELOG.json b/packages/adl-rest/CHANGELOG.json index 8bc162ede..d5d9c0187 100644 --- a/packages/adl-rest/CHANGELOG.json +++ b/packages/adl-rest/CHANGELOG.json @@ -1,6 +1,31 @@ { "name": "@azure-tools/adl-rest", "entries": [ + { + "version": "0.2.0", + "tag": "@azure-tools/adl-rest_v0.2.0", + "date": "Thu, 06 May 2021 14:56:01 GMT", + "comments": { + "minor": [ + { + "comment": "Implement alias and enum, remove model =" + } + ], + "patch": [ + { + "comment": "**Added** New type NoContentResponse" + }, + { + "comment": "Replace several internal compiler errors with diagnostics" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@azure-tools/adl\" from `0.9.0` to `0.10.0`" + } + ] + } + }, { "version": "0.1.2", "tag": "@azure-tools/adl-rest_v0.1.2", diff --git a/packages/adl-rest/CHANGELOG.md b/packages/adl-rest/CHANGELOG.md index d5cc1b2a0..a9b5412c5 100644 --- a/packages/adl-rest/CHANGELOG.md +++ b/packages/adl-rest/CHANGELOG.md @@ -1,6 +1,18 @@ # Change Log - @azure-tools/adl-rest -This log was last generated on Tue, 20 Apr 2021 15:23:29 GMT and should not be manually modified. +This log was last generated on Thu, 06 May 2021 14:56:01 GMT and should not be manually modified. + +## 0.2.0 +Thu, 06 May 2021 14:56:01 GMT + +### Minor changes + +- Implement alias and enum, remove model = + +### Patches + +- **Added** New type NoContentResponse +- Replace several internal compiler errors with diagnostics ## 0.1.2 Tue, 20 Apr 2021 15:23:29 GMT diff --git a/packages/adl-rest/package.json b/packages/adl-rest/package.json index f5ff909da..94908ed53 100644 --- a/packages/adl-rest/package.json +++ b/packages/adl-rest/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/adl-rest", - "version": "0.1.2", + "version": "0.2.0", "author": "Microsoft Corporation", "description": "ADL REST protocol binding", "homepage": "https://github.com/Azure/adl", @@ -32,7 +32,7 @@ "!dist/test/**" ], "dependencies": { - "@azure-tools/adl": "0.9.0" + "@azure-tools/adl": "0.10.0" }, "devDependencies": { "@types/node": "~14.0.27", diff --git a/packages/adl-vs/CHANGELOG.json b/packages/adl-vs/CHANGELOG.json index 69cbdab2a..94ed454e5 100644 --- a/packages/adl-vs/CHANGELOG.json +++ b/packages/adl-vs/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@azure-tools/adl-vs", "entries": [ + { + "version": "0.1.4", + "tag": "@azure-tools/adl-vs_v0.1.4", + "date": "Thu, 06 May 2021 14:56:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"adl-vscode\" from `0.4.5` to `0.5.0`" + } + ] + } + }, { "version": "0.1.3", "tag": "@azure-tools/adl-vs_v0.1.3", diff --git a/packages/adl-vs/CHANGELOG.md b/packages/adl-vs/CHANGELOG.md index 82e1ec58c..bbd853037 100644 --- a/packages/adl-vs/CHANGELOG.md +++ b/packages/adl-vs/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @azure-tools/adl-vs -This log was last generated on Tue, 20 Apr 2021 15:23:29 GMT and should not be manually modified. +This log was last generated on Thu, 06 May 2021 14:56:02 GMT and should not be manually modified. + +## 0.1.4 +Thu, 06 May 2021 14:56:02 GMT + +_Version update only_ ## 0.1.3 Tue, 20 Apr 2021 15:23:29 GMT diff --git a/packages/adl-vs/package.json b/packages/adl-vs/package.json index 17de4952e..3e8adc87e 100644 --- a/packages/adl-vs/package.json +++ b/packages/adl-vs/package.json @@ -1,7 +1,7 @@ { "name": "@azure-tools/adl-vs", "author": "Microsoft Corporation", - "version": "0.1.3", + "version": "0.1.4", "description": "ADL Language Support for Visual Studio", "homepage": "https://github.com/Azure/adl", "readme": "https://github.com/Azure/adl/blob/master/README.md", @@ -28,6 +28,6 @@ }, "dependencies": {}, "devDependencies": { - "adl-vscode": "0.4.5" + "adl-vscode": "0.5.0" } } diff --git a/packages/adl-vscode/CHANGELOG.json b/packages/adl-vscode/CHANGELOG.json index e3f6243d6..5a316b555 100644 --- a/packages/adl-vscode/CHANGELOG.json +++ b/packages/adl-vscode/CHANGELOG.json @@ -1,6 +1,23 @@ { "name": "adl-vscode", "entries": [ + { + "version": "0.5.0", + "tag": "adl-vscode_v0.5.0", + "date": "Thu, 06 May 2021 14:56:02 GMT", + "comments": { + "minor": [ + { + "comment": "Implement alias and enum, remove model =" + } + ], + "patch": [ + { + "comment": "Update syntax highlighting for string literal change" + } + ] + } + }, { "version": "0.4.5", "tag": "adl-vscode_v0.4.5", diff --git a/packages/adl-vscode/CHANGELOG.md b/packages/adl-vscode/CHANGELOG.md index 73d0c258e..48854f035 100644 --- a/packages/adl-vscode/CHANGELOG.md +++ b/packages/adl-vscode/CHANGELOG.md @@ -1,6 +1,17 @@ # Change Log - adl-vscode -This log was last generated on Tue, 20 Apr 2021 15:23:29 GMT and should not be manually modified. +This log was last generated on Thu, 06 May 2021 14:56:02 GMT and should not be manually modified. + +## 0.5.0 +Thu, 06 May 2021 14:56:02 GMT + +### Minor changes + +- Implement alias and enum, remove model = + +### Patches + +- Update syntax highlighting for string literal change ## 0.4.5 Tue, 20 Apr 2021 15:23:29 GMT diff --git a/packages/adl-vscode/package.json b/packages/adl-vscode/package.json index 5dd934a48..8514db60c 100644 --- a/packages/adl-vscode/package.json +++ b/packages/adl-vscode/package.json @@ -1,6 +1,6 @@ { "name": "adl-vscode", - "version": "0.4.5", + "version": "0.5.0", "author": "Microsoft Corporation", "description": "ADL Language Support for VS Code", "homepage": "https://github.com/Azure/adl", diff --git a/packages/adl/CHANGELOG.json b/packages/adl/CHANGELOG.json index e258bb302..30ba7d140 100644 --- a/packages/adl/CHANGELOG.json +++ b/packages/adl/CHANGELOG.json @@ -1,6 +1,50 @@ { "name": "@azure-tools/adl", "entries": [ + { + "version": "0.10.0", + "tag": "@azure-tools/adl_v0.10.0", + "date": "Thu, 06 May 2021 14:56:02 GMT", + "comments": { + "minor": [ + { + "comment": "Implement alias and enum, remove model =" + }, + { + "comment": "Implement basic parser error recovery" + }, + { + "comment": "Add API to check if a node or any descendants have parse errors" + } + ], + "patch": [ + { + "comment": "Small parsing speed improvement when expecting one of N tokens." + }, + { + "comment": "Fix blockless namespaces not accumulating decls" + }, + { + "comment": "Allow leading +/- in numeric literals and require fractional digits" + }, + { + "comment": "Fix bugs with non-ascii identifiers" + }, + { + "comment": "Improve CLI experience for generate command" + }, + { + "comment": "Replace several internal compiler errors with diagnostics" + }, + { + "comment": "Do not allow multi-line non-triple-quoted string literals" + }, + { + "comment": "Fix parsing edge cases and optimize parsing slightly" + } + ] + } + }, { "version": "0.9.0", "tag": "@azure-tools/adl_v0.9.0", diff --git a/packages/adl/CHANGELOG.md b/packages/adl/CHANGELOG.md index 0fbfefcf4..e69366772 100644 --- a/packages/adl/CHANGELOG.md +++ b/packages/adl/CHANGELOG.md @@ -1,6 +1,26 @@ # Change Log - @azure-tools/adl -This log was last generated on Tue, 20 Apr 2021 15:23:29 GMT and should not be manually modified. +This log was last generated on Thu, 06 May 2021 14:56:02 GMT and should not be manually modified. + +## 0.10.0 +Thu, 06 May 2021 14:56:02 GMT + +### Minor changes + +- Implement alias and enum, remove model = +- Implement basic parser error recovery +- Add API to check if a node or any descendants have parse errors + +### Patches + +- Small parsing speed improvement when expecting one of N tokens. +- Fix blockless namespaces not accumulating decls +- Allow leading +/- in numeric literals and require fractional digits +- Fix bugs with non-ascii identifiers +- Improve CLI experience for generate command +- Replace several internal compiler errors with diagnostics +- Do not allow multi-line non-triple-quoted string literals +- Fix parsing edge cases and optimize parsing slightly ## 0.9.0 Tue, 20 Apr 2021 15:23:29 GMT diff --git a/packages/adl/package.json b/packages/adl/package.json index 78bf6d994..68adbaad3 100644 --- a/packages/adl/package.json +++ b/packages/adl/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/adl", - "version": "0.9.0", + "version": "0.10.0", "description": "ADL Compiler Preview", "author": "Microsoft Corporation", "license": "MIT", From 69a6b60cab2a7e5c8b71ca31827827a7312dbf9a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 12 May 2021 09:46:58 -0700 Subject: [PATCH 26/47] ADL prettier plugin and `adl format` command (#511) Added a new package prettier-plugin-adl providing adl support to prettier. Just installing the plugin next to prettier will automatically enable it for .adl files. Added new adl format command. Usage: adl format **/*.adl This will format all the files matching the pattern. --- .../scripts/generate-third-party-notices.js | 2 +- packages/adl-vscode/package.json | 2 +- packages/adl/compiler/cli.ts | 11 + packages/adl/compiler/formatter.ts | 47 ++ packages/adl/compiler/index.ts | 4 + packages/adl/compiler/parser.ts | 36 +- packages/adl/compiler/scanner.ts | 4 + packages/adl/compiler/types.ts | 12 + packages/adl/formatter/index.ts | 33 + packages/adl/formatter/parser.ts | 11 + packages/adl/formatter/print/index.ts | 2 + packages/adl/formatter/print/printer.ts | 600 ++++++++++++++++++ packages/adl/formatter/print/types.ts | 20 + packages/adl/package.json | 7 +- packages/adl/test/formatter/formatter.ts | 380 +++++++++++ .../test/formatter/scenarios/inputs/alias.adl | 20 + .../test/formatter/scenarios/inputs/misc.adl | 58 ++ .../formatter/scenarios/outputs/alias.adl | 80 +++ .../test/formatter/scenarios/outputs/misc.adl | 71 +++ .../adl/test/formatter/scenarios/scenarios.ts | 63 ++ packages/prettier-plugin-adl/LICENSE | 21 + packages/prettier-plugin-adl/README.md | 32 + .../prettier-plugin-adl/ThirdPartyNotices.txt | 153 +++++ packages/prettier-plugin-adl/package.json | 28 + packages/prettier-plugin-adl/rollup.config.js | 29 + packages/prettier-plugin-adl/src/index.js | 2 + packages/prettier-plugin-adl/test/smoke.js | 11 + 27 files changed, 1732 insertions(+), 7 deletions(-) rename {packages/adl-vscode => eng}/scripts/generate-third-party-notices.js (99%) create mode 100644 packages/adl/compiler/formatter.ts create mode 100644 packages/adl/formatter/index.ts create mode 100644 packages/adl/formatter/parser.ts create mode 100644 packages/adl/formatter/print/index.ts create mode 100644 packages/adl/formatter/print/printer.ts create mode 100644 packages/adl/formatter/print/types.ts create mode 100644 packages/adl/test/formatter/formatter.ts create mode 100644 packages/adl/test/formatter/scenarios/inputs/alias.adl create mode 100644 packages/adl/test/formatter/scenarios/inputs/misc.adl create mode 100644 packages/adl/test/formatter/scenarios/outputs/alias.adl create mode 100644 packages/adl/test/formatter/scenarios/outputs/misc.adl create mode 100644 packages/adl/test/formatter/scenarios/scenarios.ts create mode 100644 packages/prettier-plugin-adl/LICENSE create mode 100644 packages/prettier-plugin-adl/README.md create mode 100644 packages/prettier-plugin-adl/ThirdPartyNotices.txt create mode 100644 packages/prettier-plugin-adl/package.json create mode 100644 packages/prettier-plugin-adl/rollup.config.js create mode 100644 packages/prettier-plugin-adl/src/index.js create mode 100644 packages/prettier-plugin-adl/test/smoke.js diff --git a/packages/adl-vscode/scripts/generate-third-party-notices.js b/eng/scripts/generate-third-party-notices.js similarity index 99% rename from packages/adl-vscode/scripts/generate-third-party-notices.js rename to eng/scripts/generate-third-party-notices.js index 6837a88c3..5101818ba 100644 --- a/packages/adl-vscode/scripts/generate-third-party-notices.js +++ b/eng/scripts/generate-third-party-notices.js @@ -116,7 +116,7 @@ function getUrl(pkg) { } async function getLicense(packageRoot) { - for (const licenseName of ["LICENSE", "LICENSE.txt", "LICENSE.md"]) { + for (const licenseName of ["LICENSE", "LICENSE.txt", "LICENSE.md", "LICENSE-MIT"]) { const licensePath = join(packageRoot, licenseName); try { let text = (await readFile(licensePath)).toString("utf-8"); diff --git a/packages/adl-vscode/package.json b/packages/adl-vscode/package.json index 8514db60c..7fc32d3a7 100644 --- a/packages/adl-vscode/package.json +++ b/packages/adl-vscode/package.json @@ -75,7 +75,7 @@ "watch-tmlanguage": "node scripts/watch-tmlanguage.js", "dogfood": "node scripts/dogfood.js", "generate-tmlanguage": "node scripts/generate-tmlanguage.js", - "generate-third-party-notices": "node scripts/generate-third-party-notices.js", + "generate-third-party-notices": "node ../../eng/scripts/generate-third-party-notices", "rollup": "rollup --config --failAfterWarnings 2>&1", "package-vsix": "vsce package --yarn" }, diff --git a/packages/adl/compiler/cli.ts b/packages/adl/compiler/cli.ts index f6211516f..c4f2c802a 100644 --- a/packages/adl/compiler/cli.ts +++ b/packages/adl/compiler/cli.ts @@ -8,6 +8,7 @@ import yargs from "yargs"; import { CompilerOptions } from "../compiler/options.js"; import { compile } from "../compiler/program.js"; import { compilerAssert, DiagnosticError, dumpError, logDiagnostics } from "./diagnostics.js"; +import { formatADLFiles } from "./formatter.js"; import { adlVersion, NodeHost } from "./util.js"; const args = yargs(process.argv.slice(2)) @@ -89,6 +90,13 @@ const args = yargs(process.argv.slice(2)) .command("install", "Install Visual Studio Extension.") .command("uninstall", "Uninstall VS Extension"); }) + .command("format ", "Format given list of adl files.", (cmd) => { + return cmd.positional("include", { + description: "Wildcard pattern of the list of files.", + type: "string", + array: true, + }); + }) .option("debug", { type: "boolean", description: "Output debug log messages.", @@ -318,6 +326,9 @@ async function main() { await uninstallVSExtension(); break; } + case "format": + await formatADLFiles(args["include"]!); + break; } } diff --git a/packages/adl/compiler/formatter.ts b/packages/adl/compiler/formatter.ts new file mode 100644 index 000000000..633ddcd3a --- /dev/null +++ b/packages/adl/compiler/formatter.ts @@ -0,0 +1,47 @@ +import { readFile, writeFile } from "fs/promises"; +import glob from "glob"; +import prettier from "prettier"; +import * as adlPrettierPlugin from "../formatter/index.js"; + +export class ADLPrettierPluginNotFound extends Error {} + +export async function formatADL(code: string): Promise { + const output = prettier.format(code, { + parser: "adl", + plugins: [adlPrettierPlugin], + }); + return output; +} + +/** + * Format all the adl files. + * @param patterns List of wildcard pattern searching for adl files. + */ +export async function formatADLFiles(patterns: string[]) { + const files = await findFiles(patterns); + for (const file of files) { + await formatADLFile(file); + } +} + +export async function formatADLFile(filename: string) { + const content = await readFile(filename); + const formattedContent = await formatADL(content.toString()); + await writeFile(filename, formattedContent); +} + +async function findFilesFromPattern(pattern: string): Promise { + return new Promise((resolve, reject) => { + glob(pattern, (err, matches) => { + if (err) { + reject(err); + } + resolve(matches); + }); + }); +} + +async function findFiles(include: string[]): Promise { + const result = await Promise.all(include.map((path) => findFilesFromPattern(path))); + return result.flat(); +} diff --git a/packages/adl/compiler/index.ts b/packages/adl/compiler/index.ts index b86579f84..2f92926c1 100644 --- a/packages/adl/compiler/index.ts +++ b/packages/adl/compiler/index.ts @@ -1,4 +1,8 @@ export * from "../lib/decorators.js"; export * from "./diagnostics.js"; +export * from "./parser.js"; export * from "./program.js"; export * from "./types.js"; + +import * as formatter from "../formatter/index.js"; +export const ADLPrettierPlugin = formatter; diff --git a/packages/adl/compiler/parser.ts b/packages/adl/compiler/parser.ts index 24543c2ad..7419a28d6 100644 --- a/packages/adl/compiler/parser.ts +++ b/packages/adl/compiler/parser.ts @@ -2,6 +2,7 @@ import { createSymbolTable } from "./binder.js"; import { compilerAssert, createDiagnostic, DiagnosticTarget, Message } from "./diagnostics.js"; import { createScanner, + isComment, isKeyword, isPunctuation, isStatementKeyword, @@ -13,6 +14,7 @@ import { ADLScriptNode, AliasStatementNode, BooleanLiteralNode, + Comment, DecoratorExpressionNode, Diagnostic, EmptyStatementNode, @@ -169,13 +171,22 @@ namespace ListKind { } as const; } -export function parse(code: string | SourceFile) { +export interface ParseOptions { + /** + * Include comments in resulting output. + */ + comments?: boolean; +} + +export function parse(code: string | SourceFile, options: ParseOptions = {}): ADLScriptNode { let parseErrorInNextFinishedNode = false; let previousTokenEnd = -1; let realPositionOfLastError = -1; let missingIdentifierCounter = 0; const parseDiagnostics: Diagnostic[] = []; const scanner = createScanner(code, reportDiagnostic); + const comments: Comment[] = []; + nextToken(); return parseADLScript(); @@ -192,6 +203,7 @@ export function parse(code: string | SourceFile) { locals: createSymbolTable(), inScopeNamespaces: [], parseDiagnostics, + comments, ...finishNode(0), }; } @@ -813,15 +825,33 @@ export function parse(code: string | SourceFile) { return scanner.tokenPosition; } + function tokenEndPos() { + return scanner.position; + } + function nextToken() { // keep track of the previous token end separately from the current scanner // position as these will differ when the previous token had trailing // trivia, and we don't want to squiggle the trivia. previousTokenEnd = scanner.position; - do { + for (;;) { scanner.scan(); - } while (isTrivia(token())); + if (isTrivia(token())) { + if (options.comments && isComment(token())) { + comments.push({ + kind: + token() === Token.SingleLineComment + ? SyntaxKind.LineComment + : SyntaxKind.BlockComment, + pos: tokenPos(), + end: tokenEndPos(), + }); + } + } else { + break; + } + } } function createMissingIdentifier(): IdentifierNode { diff --git a/packages/adl/compiler/scanner.ts b/packages/adl/compiler/scanner.ts index fb08d692a..c80118ce5 100644 --- a/packages/adl/compiler/scanner.ts +++ b/packages/adl/compiler/scanner.ts @@ -228,6 +228,10 @@ export function isTrivia(token: Token) { ); } +export function isComment(token: Token) { + return token === Token.SingleLineComment || token === Token.MultiLineComment; +} + export function isKeyword(token: Token) { return token >= MinKeyword && token <= MaxKeyword; } diff --git a/packages/adl/compiler/types.ts b/packages/adl/compiler/types.ts index e3500d369..28ab22265 100644 --- a/packages/adl/compiler/types.ts +++ b/packages/adl/compiler/types.ts @@ -197,6 +197,8 @@ export enum SyntaxKind { TemplateParameterDeclaration, EmptyStatement, InvalidStatement, + LineComment, + BlockComment, } export interface BaseNode extends TextRange { @@ -216,6 +218,15 @@ export type Node = | Statement | Expression; +export type Comment = LineComment | BlockComment; + +export interface LineComment extends TextRange { + kind: SyntaxKind.LineComment; +} +export interface BlockComment extends TextRange { + kind: SyntaxKind.BlockComment; +} + export interface ADLScriptNode extends BaseNode { kind: SyntaxKind.ADLScript; statements: Statement[]; @@ -226,6 +237,7 @@ export interface ADLScriptNode extends BaseNode { namespaces: NamespaceStatementNode[]; // list of namespaces in this file (initialized during binding) locals: SymbolTable; usings: UsingStatementNode[]; + comments: Comment[]; parseDiagnostics: Diagnostic[]; } diff --git a/packages/adl/formatter/index.ts b/packages/adl/formatter/index.ts new file mode 100644 index 000000000..5e0e941b5 --- /dev/null +++ b/packages/adl/formatter/index.ts @@ -0,0 +1,33 @@ +import { Parser, SupportLanguage } from "prettier"; +import { Node } from "../compiler/types.js"; +import { parse } from "./parser.js"; +import { ADLPrinter } from "./print/index.js"; + +export const defaultOptions = {}; + +export const languages: SupportLanguage[] = [ + { + name: "ADL", + parsers: ["adl"], + extensions: [".adl"], + vscodeLanguageIds: ["adl"], + }, +]; + +const ADLParser: Parser = { + parse, + astFormat: "adl-format", + locStart(node: Node) { + return node.pos; + }, + locEnd(node: Node) { + return node.end; + }, +}; +export const parsers = { + adl: ADLParser, +}; + +export const printers = { + "adl-format": ADLPrinter, +}; diff --git a/packages/adl/formatter/parser.ts b/packages/adl/formatter/parser.ts new file mode 100644 index 000000000..42efb01d8 --- /dev/null +++ b/packages/adl/formatter/parser.ts @@ -0,0 +1,11 @@ +import { Parser, ParserOptions } from "prettier"; +import { parse as adlParse } from "../compiler/parser.js"; +import { ADLScriptNode } from "../compiler/types.js"; + +export function parse( + text: string, + parsers: { [parserName: string]: Parser }, + opts: ParserOptions & { parentParser?: string } +): ADLScriptNode { + return adlParse(text, { comments: true }); +} diff --git a/packages/adl/formatter/print/index.ts b/packages/adl/formatter/print/index.ts new file mode 100644 index 000000000..75e501440 --- /dev/null +++ b/packages/adl/formatter/print/index.ts @@ -0,0 +1,2 @@ +export * from "./printer.js"; +export * from "./types.js"; diff --git a/packages/adl/formatter/print/printer.ts b/packages/adl/formatter/print/printer.ts new file mode 100644 index 000000000..3cb2a0212 --- /dev/null +++ b/packages/adl/formatter/print/printer.ts @@ -0,0 +1,600 @@ +import prettier, { Doc, FastPath, Printer } from "prettier"; +import { + ADLScriptNode, + AliasStatementNode, + BlockComment, + Comment, + DecoratorExpressionNode, + EnumMemberNode, + EnumStatementNode, + IntersectionExpressionNode, + ModelExpressionNode, + ModelPropertyNode, + ModelSpreadPropertyNode, + ModelStatementNode, + NamespaceStatementNode, + Node, + OperationStatementNode, + Statement, + SyntaxKind, + TypeReferenceNode, + UnionExpressionNode, +} from "../../compiler/types.js"; +import { ADLPrettierOptions, DecorableNode, PrettierChildPrint } from "./types.js"; + +const { + align, + breakParent, + concat, + group, + hardline, + ifBreak, + indent, + join, + line, + softline, +} = prettier.doc.builders; + +const { isNextLineEmpty } = prettier.util; +const { replaceNewlinesWithLiterallines } = prettier.doc.utils as any; + +export const ADLPrinter: Printer = { + print: printADL, + canAttachComment: canAttachComment, + printComment: printComment, +}; + +export function printADL( + // Path to the AST node to print + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +): prettier.Doc { + const node: Node = path.getValue(); + + switch (node.kind) { + // Root + case SyntaxKind.ADLScript: + return printStatementSequence(path as FastPath, options, print, "statements"); + + // Statements + case SyntaxKind.ImportStatement: + return concat([`import "${node.path.value}";`]); + case SyntaxKind.UsingStatement: + return concat([`using "`, path.call(print, "name"), `";`]); + case SyntaxKind.OperationStatement: + return printOperationStatement(path as FastPath, options, print); + case SyntaxKind.NamespaceStatement: + return printNamespaceStatement(path as FastPath, options, print); + case SyntaxKind.ModelStatement: + return printModelStatement(path as FastPath, options, print); + case SyntaxKind.AliasStatement: + return printAliasStatement(path as FastPath, options, print); + case SyntaxKind.EnumStatement: + return printEnumStatement(path as FastPath, options, print); + + // Others. + case SyntaxKind.Identifier: + return node.sv; + case SyntaxKind.StringLiteral: + return `"${node.value}"`; + case SyntaxKind.NumericLiteral: + return `${node.value}`; + case SyntaxKind.ModelExpression: + return printModelExpression(path as FastPath, options, print); + case SyntaxKind.ModelProperty: + return printModelProperty(path as FastPath, options, print); + case SyntaxKind.DecoratorExpression: + return printDecorator(path as FastPath, options, print); + case SyntaxKind.UnionExpression: + return printUnion(path as FastPath, options, print); + case SyntaxKind.IntersectionExpression: + return printIntersection(path as FastPath, options, print); + case SyntaxKind.EnumMember: + return printEnumMember(path as FastPath, options, print); + case SyntaxKind.TypeReference: + return printTypeReference(path as FastPath, options, print); + default: + return options.originalText.slice(node.pos, node.end); + } +} + +export function printAliasStatement( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +) { + const node = path.getValue(); + const id = path.call(print, "id"); + const template = + node.templateParameters.length === 0 + ? "" + : concat(["<", join(", ", path.map(print, "templateParameters")), ">"]); + return concat(["alias ", id, template, " = ", path.call(print, "value"), ";"]); +} + +export function canAttachComment(node: Node): boolean { + const kind = node.kind as SyntaxKind; + return Boolean(kind && kind !== SyntaxKind.LineComment && kind !== SyntaxKind.BlockComment); +} + +export function printComment( + commentPath: FastPath, + options: ADLPrettierOptions +): Doc { + const comment = commentPath.getValue(); + + switch (comment.kind) { + case SyntaxKind.BlockComment: + return printBlockComment(commentPath as FastPath, options); + case SyntaxKind.LineComment: + return `${options.originalText.slice(comment.pos, comment.end).trimRight()}`; + default: + throw new Error(`Not a comment: ${JSON.stringify(comment)}`); + } +} + +function printBlockComment(commentPath: FastPath, options: ADLPrettierOptions) { + const comment = commentPath.getValue(); + const rawComment = options.originalText.slice(comment.pos + 2, comment.end - 2); + + if (isIndentableBlockComment(rawComment)) { + const printed = printIndentableBlockComment(rawComment); + return printed; + } + + return concat(["/*", replaceNewlinesWithLiterallines(rawComment), "*/"]); +} + +function isIndentableBlockComment(rawComment: string): boolean { + // If the comment has multiple lines and every line starts with a star + // we can fix the indentation of each line. The stars in the `/*` and + // `*/` delimiters are not included in the comment value, so add them + // back first. + const lines = `*${rawComment}*`.split("\n"); + return lines.length > 1 && lines.every((line) => line.trim()[0] === "*"); +} + +function printIndentableBlockComment(rawComment: string): Doc { + const lines = rawComment.split("\n"); + + return concat([ + "/*", + join( + hardline, + lines.map((line, index) => + index === 0 + ? line.trimEnd() + : " " + (index < lines.length - 1 ? line.trim() : line.trimStart()) + ) + ), + "*/", + ]); +} + +export function printDecorators( + path: FastPath, + options: object, + print: PrettierChildPrint, + { tryInline }: { tryInline: boolean } +) { + const node = path.getValue(); + if (node.decorators.length === 0) { + return ""; + } + + const decorators = path.map((x) => concat([print(x as any), ifBreak(line, " ")]), "decorators"); + const shouldBreak = tryInline && decorators.length < 3 ? "" : breakParent; + + return group(concat([...decorators, shouldBreak])); +} + +export function printDecorator( + path: FastPath, + options: object, + print: PrettierChildPrint +) { + const args = printDecoratorArgs(path, options, print); + return concat(["@", path.call(print, "target"), args]); +} + +function printDecoratorArgs( + path: FastPath, + options: object, + print: PrettierChildPrint +) { + const node = path.getValue(); + if (node.arguments.length === 0) { + return ""; + } + + // So that decorator with single object arguments have ( and { hugging. + // @deco({ + // value: "foo" + // }) + const shouldHug = + node.arguments.length === 1 && node.arguments[0].kind === SyntaxKind.ModelExpression; + + if (shouldHug) { + return concat([ + "(", + join( + ", ", + path.map((arg) => concat([print(arg)]), "arguments") + ), + ")", + ]); + } + + return concat([ + "(", + group( + concat([ + indent( + join( + ", ", + path.map((arg) => concat([softline, print(arg)]), "arguments") + ) + ), + softline, + ]) + ), + ")", + ]); +} + +export function printEnumStatement( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +) { + const id = path.call(print, "id"); + const decorators = printDecorators(path, options, print, { tryInline: false }); + return concat([decorators, "enum ", id, " ", printEnumBlock(path, options, print)]); +} + +function printEnumBlock( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +) { + const node = path.getValue(); + if (node.members.length === 0) { + return "{}"; + } + + return group( + concat([ + "{", + indent( + concat([ + hardline, + join( + hardline, + path.map((x) => concat([print(x as any), ","]), "members") + ), + ]) + ), + hardline, + "}", + ]) + ); +} + +export function printEnumMember( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +) { + const node = path.getValue(); + const id = path.call(print, "id"); + const value = node.value ? concat([": ", path.call(print, "value")]) : ""; + const decorators = printDecorators(path, options, print, { tryInline: true }); + return concat([decorators, id, value]); +} + +/** + * Handle printing an intersection node. + * @example `Foo & Bar` or `{foo: string} & {bar: string}` + * + * @param path Prettier AST Path. + * @param options Prettier options + * @param print Prettier child print callback. + * @returns Prettier document. + */ +export function printIntersection( + path: FastPath, + options: object, + print: PrettierChildPrint +) { + const node = path.getValue(); + const types = path.map(print, "options"); + const result: (prettier.Doc | string)[] = []; + let wasIndented = false; + for (let i = 0; i < types.length; ++i) { + if (i === 0) { + result.push(types[i]); + } else if (isModelNode(node.options[i - 1]) && isModelNode(node.options[i])) { + // If both are objects, don't indent + result.push(concat([" & ", wasIndented ? indent(types[i]) : types[i]])); + } else if (!isModelNode(node.options[i - 1]) && !isModelNode(node.options[i])) { + // If no object is involved, go to the next line if it breaks + result.push(indent(concat([" &", line, types[i]]))); + } else { + // If you go from object to non-object or vis-versa, then inline it + if (i > 1) { + wasIndented = true; + } + result.push(" & ", i > 1 ? indent(types[i]) : types[i]); + } + } + return group(concat(result)); +} + +function isModelNode(node: Node) { + return node.kind === SyntaxKind.ModelExpression; +} + +export function printModelExpression( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +) { + const inBlock = isModelExpressionInBlock(path); + + if (inBlock) { + return group(printModelPropertiesBlock(path, options, print)); + } else { + return group( + concat([ + indent( + join( + ", ", + path.map((arg) => concat([softline, print(arg)]), "properties") + ) + ), + softline, + ]) + ); + } +} + +export function printModelStatement( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +) { + const node = path.getValue(); + const id = path.call(print, "id"); + const heritage = + node.heritage.length > 0 ? concat(["extends ", path.map(print, "heritage")[0], " "]) : ""; + + return concat([ + printDecorators(path, options, print, { tryInline: false }), + "model ", + id, + " ", + heritage, + printModelPropertiesBlock(path, options, print), + ]); +} + +function printModelPropertiesBlock( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +) { + const node = path.getNode(); + if (!node?.properties || node.properties.length === 0) { + return "{}"; + } + + const seperator = isModelAValue(path) ? "," : ";"; + + return concat([ + "{", + indent( + concat([ + hardline, + join( + hardline, + path.map((x) => concat([print(x as any), seperator]), "properties") + ), + ]) + ), + hardline, + "}", + ]); +} + +/** + * Figure out if this model is being used as a definition or value. + * @returns true if the model is used as a value(e.g. decorator value), false if it is used as a model definition. + */ +function isModelAValue(path: FastPath): boolean { + let count = 0; + let node: Node | null = path.getValue(); + do { + switch (node.kind) { + case SyntaxKind.ModelStatement: + case SyntaxKind.AliasStatement: + return false; + case SyntaxKind.DecoratorExpression: + return true; + } + } while ((node = path.getParentNode(count++))); + return true; +} + +export function printModelProperty( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +) { + const node = path.getValue(); + return concat([ + printDecorators(path as FastPath, options, print, { tryInline: true }), + path.call(print, "id"), + node.optional ? "?: " : ": ", + path.call(print, "value"), + ]); +} + +function isModelExpressionInBlock(path: FastPath) { + const parent: Node | null = path.getParentNode() as any; + + switch (parent?.kind) { + case SyntaxKind.OperationStatement: + return false; + default: + return true; + } +} + +export function printNamespaceStatement( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +) { + const printNamespace = ( + path: FastPath, + names: Doc[], + suffix: Doc | string + ) => {}; + + const printNested = (currentPath: FastPath, parentNames: Doc[]): Doc => { + const names = [...parentNames, currentPath.call(print, "name")]; + const currentNode = currentPath.getNode(); + + if ( + !Array.isArray(currentNode?.statements) && + currentNode?.statements?.kind === SyntaxKind.NamespaceStatement + ) { + return path.call((x) => printNested(x, names), "statements"); + } + + const suffix = + currentNode?.statements === undefined + ? ";" + : concat([ + " {", + indent(concat([hardline, printStatementSequence(path, options, print, "statements")])), + hardline, + "}", + ]); + return concat([ + printDecorators(path, options, print, { tryInline: false }), + `namespace `, + join(".", names), + suffix, + ]); + }; + + return printNested(path, []); +} + +export function printOperationStatement( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +) { + return concat([ + printDecorators(path as FastPath, options, print, { tryInline: true }), + `op `, + path.call(print, "id"), + "(", + path.call(print, "parameters"), + "): ", + path.call(print, "returnType"), + `;`, + ]); +} + +export function printStatementSequence( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint, + property: keyof T +) { + const node = path.getValue(); + const parts: Doc[] = []; + const lastStatement = getLastStatement((node[property] as any) as Statement[]); + + path.each((statementPath) => { + const node = path.getValue(); + + if (node.kind === SyntaxKind.EmptyStatement) { + return; + } + + const printed = print(statementPath); + parts.push(printed); + + if (node !== lastStatement) { + parts.push(hardline); + + if (isNextLineEmpty(options.originalText, node, options.locEnd)) { + parts.push(hardline); + } + } + }, property); + + return concat(parts); +} + +function getLastStatement(statements: Statement[]): Statement | undefined { + for (let i = statements.length - 1; i >= 0; i--) { + const statement = statements[i]; + if (statement.kind !== SyntaxKind.EmptyStatement) { + return statement; + } + } + return undefined; +} + +export function printUnion( + path: FastPath, + options: object, + print: PrettierChildPrint +) { + const node = path.getValue(); + const shouldHug = shouldHugType(node); + + const types = path.map((typePath) => { + let printedType: string | prettier.Doc = print(typePath); + if (!shouldHug) { + printedType = align(2, printedType); + } + return printedType; + }, "options"); + + if (shouldHug) { + return join(" | ", types); + } + + const shouldAddStartLine = true; + const code = [ + ifBreak(concat([shouldAddStartLine ? line : "", "| "]), ""), + join(concat([line, "| "]), types), + ]; + return group(indent(concat(code))); +} + +function shouldHugType(node: Node) { + if (node.kind === SyntaxKind.UnionExpression || node.kind === SyntaxKind.IntersectionExpression) { + return node.options.length < 4; + } + return false; +} + +export function printTypeReference( + path: prettier.FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint +): prettier.doc.builders.Doc { + const node = path.getValue(); + const type = path.call(print, "target"); + const template = + node.arguments.length === 0 ? "" : concat(["<", join(", ", path.map(print, "arguments")), ">"]); + return concat([type, template]); +} diff --git a/packages/adl/formatter/print/types.ts b/packages/adl/formatter/print/types.ts new file mode 100644 index 000000000..b942ce6fa --- /dev/null +++ b/packages/adl/formatter/print/types.ts @@ -0,0 +1,20 @@ +import { Doc, FastPath, ParserOptions } from "prettier"; +import { DecoratorExpressionNode, Node } from "../../compiler/types.js"; + +export interface ADLPrettierOptions extends ParserOptions {} + +export type PrettierChildPrint = (path: FastPath, index?: number) => Doc; + +export interface DecorableNode { + decorators: DecoratorExpressionNode[]; +} + +type Union = + | { foo: string } + | { bar: string } + | { bar: string } + | { bar: string } + | { bar: string }; +type Inter = { foo: string } & { bar: string } & { bar: string } & { bar: string } & { + bar: string; +}; diff --git a/packages/adl/package.json b/packages/adl/package.json index 68adbaad3..7949067ef 100644 --- a/packages/adl/package.json +++ b/packages/adl/package.json @@ -45,21 +45,24 @@ }, "dependencies": { "autorest": "~3.0.6335", + "glob": "~7.1.6", "mkdirp": "~1.0.4", + "prettier": "~2.2.1", "resolve": "~1.20.0", - "vscode-languageserver": "~7.0.0", "vscode-languageserver-textdocument": "~1.0.1", + "vscode-languageserver": "~7.0.0", "yargs": "~16.2.0" }, "devDependencies": { + "@types/glob": "~7.1.3", "@types/mkdirp": "~1.0.1", "@types/mocha": "~7.0.2", "@types/node": "~14.0.27", + "@types/prettier": "^2.0.2", "@types/resolve": "~1.20.0", "@types/yargs": "~15.0.12", "grammarkdown": "~3.1.2", "mocha": "~8.3.2", - "prettier": "~2.2.1", "prettier-plugin-organize-imports": "~1.1.1", "source-map-support": "~0.5.19", "typescript": "~4.2.4" diff --git a/packages/adl/test/formatter/formatter.ts b/packages/adl/test/formatter/formatter.ts new file mode 100644 index 000000000..352833ede --- /dev/null +++ b/packages/adl/test/formatter/formatter.ts @@ -0,0 +1,380 @@ +import { strictEqual } from "assert"; +import prettier from "prettier"; +import * as plugin from "../../formatter/index.js"; + +function format(code: string): string { + const output = prettier.format(code, { + parser: "adl", + plugins: [plugin], + }); + return output; +} + +function assertFormat({ code, expected }: { code: string; expected: string }) { + const result = format(code); + strictEqual(result, expected.trim()); +} + +describe("adl: prettier formatter", () => { + it("format imports", () => { + assertFormat({ + code: ` + import "@azure-tools/adl-rest"; +import "@azure-tools/adl-openapi"; +import "@azure-tools/adl-rpaas" ; +`, + expected: ` +import "@azure-tools/adl-rest"; +import "@azure-tools/adl-openapi"; +import "@azure-tools/adl-rpaas"; +`, + }); + }); + + describe("model", () => { + it("format simple models", () => { + assertFormat({ + code: ` +model Foo { + id: number; + type: Bar; + + name?: string; + isArray: string[] ; +} +`, + expected: ` +model Foo { + id: number; + type: Bar; + name?: string; + isArray: string[]; +} +`, + }); + }); + + it("format nested models", () => { + assertFormat({ + code: ` +model Foo { + id: number; + address: { street: string, country: string} +} +`, + expected: ` +model Foo { + id: number; + address: { + street: string; + country: string; + }; +} +`, + }); + }); + + it("format models with spread", () => { + assertFormat({ + code: ` +model Foo { + id: number; + ...Bar; + name: string; +} +`, + expected: ` +model Foo { + id: number; + ...Bar; + name: string; +} +`, + }); + }); + + it("format model with decorator", () => { + assertFormat({ + code: ` + @some @decorator +model Foo {} +`, + expected: ` +@some +@decorator +model Foo {} +`, + }); + }); + + it("format model with heritage", () => { + assertFormat({ + code: ` +model Foo extends Base { +} + +model Bar extends Base< + string > { +} +`, + expected: ` +model Foo extends Base {} + +model Bar extends Base {} +`, + }); + }); + }); + + describe("comments", () => { + it("format single line comments", () => { + assertFormat({ + code: ` + // This is a comment. +model Foo {} +`, + expected: ` +// This is a comment. +model Foo {} +`, + }); + }); + + it("format multi line comments", () => { + assertFormat({ + code: ` + /** + * This is a multiline comment + * that has bad formatting. + */ +model Foo {} +`, + expected: ` +/** + * This is a multiline comment + * that has bad formatting. + */ +model Foo {} +`, + }); + }); + }); + + describe("alias union", () => { + it("format simple alias", () => { + assertFormat({ + code: ` +alias Foo = "one" | "two"; +alias Bar + = "one" + | "two"; +`, + expected: ` +alias Foo = "one" | "two"; +alias Bar = "one" | "two"; +`, + }); + }); + + it("format generic alias", () => { + assertFormat({ + code: ` +alias Foo< A, B> = A | B +alias Bar< + A, B> = + A | + B +`, + expected: ` +alias Foo = A | B; +alias Bar = A | B; +`, + }); + }); + + it("format long alias", () => { + assertFormat({ + code: ` +alias VeryLong = "one" | "two" | "three" | "four" | "five" | "six" | "seven" | "height" | "nine" | "ten"; +`, + expected: ` +alias VeryLong = + | "one" + | "two" + | "three" + | "four" + | "five" + | "six" + | "seven" + | "height" + | "nine" + | "ten"; +`, + }); + }); + }); + + describe("alias intersection", () => { + it("format intersection of types", () => { + assertFormat({ + code: ` +alias Foo = One & Two; +alias Bar + = One & + Two; +`, + expected: ` +alias Foo = One & Two; +alias Bar = One & Two; +`, + }); + }); + + it("format intersection of anoymous models", () => { + assertFormat({ + code: ` +alias Foo = { foo: string } & {bar: string}; +alias Bar + = { foo: string } & + { + bar: string}; +`, + expected: ` +alias Foo = { + foo: string; +} & { + bar: string; +}; +alias Bar = { + foo: string; +} & { + bar: string; +}; +`, + }); + }); + }); + + describe("enum", () => { + it("format simple enum", () => { + assertFormat({ + code: ` +enum Foo { A, B} +enum Bar + { A, + B} +`, + expected: ` +enum Foo { + A, + B, +} +enum Bar { + A, + B, +} +`, + }); + }); + + it("format named enum", () => { + assertFormat({ + code: ` +enum Foo { A: "a", B : "b"} +enum Bar + { A: "a", + B: + "b"} +`, + expected: ` +enum Foo { + A: "a", + B: "b", +} +enum Bar { + A: "a", + B: "b", +} +`, + }); + }); + }); + + describe("namespaces", () => { + it("format global namespace", () => { + assertFormat({ + code: ` +namespace Foo; + +namespace Foo . Bar; +`, + expected: ` +namespace Foo; + +namespace Foo.Bar; +`, + }); + }); + + it("format global namespace with decorators", () => { + assertFormat({ + code: ` + @service + @other +namespace Foo . Bar; +`, + expected: ` +@service +@other +namespace Foo.Bar; +`, + }); + }); + + it("format namespace with body", () => { + assertFormat({ + code: ` +namespace Foo { + op some(): string; +} + + +namespace Foo . Bar { + op some(): string; +} +`, + expected: ` +namespace Foo { + op some(): string; +} + +namespace Foo.Bar { + op some(): string; +} +`, + }); + }); + + it("format nested namespaces", () => { + assertFormat({ + code: ` +namespace Foo { + +namespace Foo . Bar { +op some(): string; +} +} + + +`, + expected: ` +namespace Foo { + namespace Foo.Bar { + op some(): string; + } +} +`, + }); + }); + }); +}); diff --git a/packages/adl/test/formatter/scenarios/inputs/alias.adl b/packages/adl/test/formatter/scenarios/inputs/alias.adl new file mode 100644 index 000000000..d06d9452e --- /dev/null +++ b/packages/adl/test/formatter/scenarios/inputs/alias.adl @@ -0,0 +1,20 @@ +alias UnionOfType = Foo | Bar; + +alias UnionOfManyType = "one" | "two" | "three" | "four" | "five" | "six" | "seven" | "height" | "nine" | "ten"; + +alias UnionOfObject = {foo: string} | {bar: string}; + +alias UnionOfManyObject = {one: string} | {two: string} | {three: string} | {four: string} | {five: string}; + +alias UnionOfMix = Foo | { bar: string}; + +alias UnionOfManyMix = Foo | { bar: string} | Bar | Other | {other: string}; + +alias InterOfObject = {foo: string} & {bar: string}; + +alias InterOfManyObject = {one: string} & {two: string} & {three: string} & {four: string} & {five: string}; + +alias InterOfMix = Foo & { bar: string}; + +alias InterOfManyMix = Foo & { bar: string} & Bar & Other & {other: string}; + diff --git a/packages/adl/test/formatter/scenarios/inputs/misc.adl b/packages/adl/test/formatter/scenarios/inputs/misc.adl new file mode 100644 index 000000000..50ccb47f4 --- /dev/null +++ b/packages/adl/test/formatter/scenarios/inputs/misc.adl @@ -0,0 +1,58 @@ +import "@azure-tools/adl-rest"; +import "@azure-tools/adl-openapi"; +import "@azure-tools/adl-rpaas"; + +alias Bar = "that" | "this"; + +// This is a comment +alias VeryLong = "one" | "two" | "three" | "four" | "five" | "six" | "seven" | "height" | "nine" | "ten"; + +alias Inter = {foo: string} & {bar: string} ; + + +alias Inter2 = Foo & +Inter; + +alias Generic< A, B> = A | B; + +/** + * Model comment + */ +model Foo { + id: number; + type: Bar; + + /* + * Name is special and comment is unaligned. + */ + name?: string; + isArray: string[] ; + + address: { street: string, country: string}; + + ...Bar; +} + + +@bar +enum SomeEnum { A, B, C} + +enum + SomeNamedEnum { A: "a", + B: "b", @val() C: "c" } + + + +/** + * Multi line comment still works, + * yeahh.... + */ +@resource("/operations") +namespace Operations { + // Comment here too + @get @doc("Abc") op get(@path id: string): ArmResponse; + + + + @get op list(): ArmResponse | ErrorResponse; +} diff --git a/packages/adl/test/formatter/scenarios/outputs/alias.adl b/packages/adl/test/formatter/scenarios/outputs/alias.adl new file mode 100644 index 000000000..fc3a61eda --- /dev/null +++ b/packages/adl/test/formatter/scenarios/outputs/alias.adl @@ -0,0 +1,80 @@ +alias UnionOfType = Foo | Bar; + +alias UnionOfManyType = + | "one" + | "two" + | "three" + | "four" + | "five" + | "six" + | "seven" + | "height" + | "nine" + | "ten"; + +alias UnionOfObject = { + foo: string; +} | { + bar: string; +}; + +alias UnionOfManyObject = + | { + one: string; + } + | { + two: string; + } + | { + three: string; + } + | { + four: string; + } + | { + five: string; + }; + +alias UnionOfMix = Foo | { + bar: string; +}; + +alias UnionOfManyMix = + | Foo + | { + bar: string; + } + | Bar + | Other + | { + other: string; + }; + +alias InterOfObject = { + foo: string; +} & { + bar: string; +}; + +alias InterOfManyObject = { + one: string; +} & { + two: string; +} & { + three: string; +} & { + four: string; +} & { + five: string; +}; + +alias InterOfMix = Foo & { + bar: string; +}; + +alias InterOfManyMix = Foo & { + bar: string; +} & Bar & + Other & { + other: string; + }; \ No newline at end of file diff --git a/packages/adl/test/formatter/scenarios/outputs/misc.adl b/packages/adl/test/formatter/scenarios/outputs/misc.adl new file mode 100644 index 000000000..16cd6fcbc --- /dev/null +++ b/packages/adl/test/formatter/scenarios/outputs/misc.adl @@ -0,0 +1,71 @@ +import "@azure-tools/adl-rest"; +import "@azure-tools/adl-openapi"; +import "@azure-tools/adl-rpaas"; + +alias Bar = "that" | "this"; + +// This is a comment +alias VeryLong = + | "one" + | "two" + | "three" + | "four" + | "five" + | "six" + | "seven" + | "height" + | "nine" + | "ten"; + +alias Inter = { + foo: string; +} & { + bar: string; +}; + +alias Inter2 = Foo & Inter; + +alias Generic = A | B; + +/** + * Model comment + */ +model Foo { + id: number; + type: Bar; + /* + * Name is special and comment is unaligned. + */ + name?: string; + isArray: string[]; + address: { + street: string; + country: string; + }; + ...Bar; +} + +@bar +enum SomeEnum { + A, + B, + C, +} + +enum SomeNamedEnum { + A: "a", + B: "b", + @val C: "c", +} + +/** + * Multi line comment still works, + * yeahh.... + */ +@resource("/operations") +namespace Operations { + // Comment here too + @get @doc("Abc") op get(@path id: string): ArmResponse; + + @get op list(): ArmResponse | ErrorResponse; +} \ No newline at end of file diff --git a/packages/adl/test/formatter/scenarios/scenarios.ts b/packages/adl/test/formatter/scenarios/scenarios.ts new file mode 100644 index 000000000..6fd469614 --- /dev/null +++ b/packages/adl/test/formatter/scenarios/scenarios.ts @@ -0,0 +1,63 @@ +import { strictEqual } from "assert"; +import { mkdir, readFile, writeFile } from "fs/promises"; +import { dirname, join, resolve } from "path"; +import prettier from "prettier"; +import { fileURLToPath } from "url"; +import * as plugin from "../../../formatter/index.js"; + +function format(code: string): string { + const output = prettier.format(code, { + parser: "adl", + plugins: [plugin], + }); + return output; +} + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const scenarioRoot = resolve(__dirname, "../../../../test/formatter/scenarios"); +const shouldUpdate = process.argv.indexOf("--update-snapshots") !== -1; + +async function getOutput(name: string): Promise { + try { + const output = await readFile(join(scenarioRoot, "outputs", name)); + return output.toString(); + } catch { + return undefined; + } +} + +async function saveOutput(name: string, content: string) { + const outputDir = join(scenarioRoot, "outputs"); + await mkdir(outputDir, { recursive: true }); + await writeFile(join(outputDir, name), content); +} + +async function testScenario(name: string) { + const content = await readFile(join(scenarioRoot, "inputs", name)); + const output = await getOutput(name); + const formatted = format(content.toString()); + if (!output) { + return await saveOutput(name, formatted); + } + if (output !== formatted) { + if (shouldUpdate) { + return await saveOutput(name, formatted); + } + + strictEqual( + formatted, + output, + `Scenario ${name} does not match expected snapshot. Run with tests '--update-snapshots' option to update.` + ); + } +} + +describe("Format scenarios", () => { + it("misc", async () => { + await testScenario("misc.adl"); + }); + + it("alias", async () => { + await testScenario("alias.adl"); + }); +}); diff --git a/packages/prettier-plugin-adl/LICENSE b/packages/prettier-plugin-adl/LICENSE new file mode 100644 index 000000000..21071075c --- /dev/null +++ b/packages/prettier-plugin-adl/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/packages/prettier-plugin-adl/README.md b/packages/prettier-plugin-adl/README.md new file mode 100644 index 000000000..5e12ccec1 --- /dev/null +++ b/packages/prettier-plugin-adl/README.md @@ -0,0 +1,32 @@ +# Prettier Plugin for ADL + +## Requirements + +- **Using node 14 and above** +- For use in **VSCode**, use version `VSCode 1.56` and above. + +## Getting Started + +```bash +npm install --save-dev prettier @azure-tools/prettier-plugin-adl +``` + +You can now call prettier + +```bash +./node_modules/.bin/prettier --write '**/*.adl' +``` + +# Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/packages/prettier-plugin-adl/ThirdPartyNotices.txt b/packages/prettier-plugin-adl/ThirdPartyNotices.txt new file mode 100644 index 000000000..a2c885cf9 --- /dev/null +++ b/packages/prettier-plugin-adl/ThirdPartyNotices.txt @@ -0,0 +1,153 @@ +prettier-plugin-adl + +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION +Do Not Translate or Localize + +This project incorporates components from the projects listed below. The +original copyright notices and the licenses under which Microsoft received such +components are set forth below. Microsoft reserves all rights not expressly +granted herein, whether by implication, estoppel or otherwise. + +1. function-bind version 1.1.1 (https://github.com/Raynos/function-bind) +2. has version 1.0.3 (https://github.com/tarruda/has) +3. is-core-module version 2.2.0 (https://github.com/inspect-js/is-core-module) +4. path-parse version 1.0.6 (https://github.com/jbgutierrez/path-parse) +5. resolve version 1.20.0 (https://github.com/browserify/resolve) + + +%% function-bind NOTICES AND INFORMATION BEGIN HERE +===================================================== +Copyright (c) 2013 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +====================================================="); +END OF function-bind NOTICES AND INFORMATION + + +%% has NOTICES AND INFORMATION BEGIN HERE +===================================================== +Copyright (c) 2013 Thiago de Arruda + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +====================================================="); +END OF has NOTICES AND INFORMATION + + +%% is-core-module NOTICES AND INFORMATION BEGIN HERE +===================================================== +The MIT License (MIT) + +Copyright (c) 2014 Dave Justice + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +====================================================="); +END OF is-core-module NOTICES AND INFORMATION + + +%% path-parse NOTICES AND INFORMATION BEGIN HERE +===================================================== +The MIT License (MIT) + +Copyright (c) 2015 Javier Blanco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +====================================================="); +END OF path-parse NOTICES AND INFORMATION + + +%% resolve NOTICES AND INFORMATION BEGIN HERE +===================================================== +MIT License + +Copyright (c) 2012 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +====================================================="); +END OF resolve NOTICES AND INFORMATION \ No newline at end of file diff --git a/packages/prettier-plugin-adl/package.json b/packages/prettier-plugin-adl/package.json new file mode 100644 index 000000000..c85897c5f --- /dev/null +++ b/packages/prettier-plugin-adl/package.json @@ -0,0 +1,28 @@ +{ + "name": "@azure-tools/prettier-plugin-adl", + "version": "0.1.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "build": "rollup --config 2>&1 && npm run generate-third-party-notices", + "test": "node ./test/smoke.js", + "generate-third-party-notices": "node ../../eng/scripts/generate-third-party-notices" + }, + "author": "Microsoft Corporation", + "license": "MIT", + "dependencies": { + "prettier": "~2.2.1" + }, + "devDependencies": { + "@azure-tools/adl": "0.10.0", + "@rollup/plugin-commonjs": "~17.1.0", + "@rollup/plugin-json": "~4.1.0", + "@rollup/plugin-node-resolve": "~11.2.0", + "@rollup/plugin-replace": "~2.4.2", + "rollup": "~2.41.4" + }, + "files": [ + "dist/**/*", + "ThirdPartyNotices.txt" + ] +} diff --git a/packages/prettier-plugin-adl/rollup.config.js b/packages/prettier-plugin-adl/rollup.config.js new file mode 100644 index 000000000..80e4166fd --- /dev/null +++ b/packages/prettier-plugin-adl/rollup.config.js @@ -0,0 +1,29 @@ +// @ts-check +import resolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import json from "@rollup/plugin-json"; +import replace from "@rollup/plugin-replace"; + +export default { + input: "src/index.js", + output: { + file: "dist/index.js", + format: "commonjs", + sourcemap: true, + exports: "default", + }, + context: "this", + external: ["fs/promises", "prettier"], + plugins: [ + resolve({ preferBuiltins: true }), + commonjs(), + json(), + replace({ + values: { + "export const adlVersion = getVersion();": `export const adlVersion = "";`, + }, + delimiters: ["", ""], + preventAssignment: true, + }), + ], +}; diff --git a/packages/prettier-plugin-adl/src/index.js b/packages/prettier-plugin-adl/src/index.js new file mode 100644 index 000000000..873b53ec8 --- /dev/null +++ b/packages/prettier-plugin-adl/src/index.js @@ -0,0 +1,2 @@ +const { ADLPrettierPlugin } = require("@azure-tools/adl"); +module.exports = ADLPrettierPlugin; diff --git a/packages/prettier-plugin-adl/test/smoke.js b/packages/prettier-plugin-adl/test/smoke.js new file mode 100644 index 000000000..7a24df1e3 --- /dev/null +++ b/packages/prettier-plugin-adl/test/smoke.js @@ -0,0 +1,11 @@ +// Simple smoke test verifying the plugin is able to be loaded via prettier. +const prettier = require("prettier"); + +const result = prettier.format("alias Foo = string", { + parser: "adl", + plugins: ["."], +}); + +if (result !== "alias Foo = string;") { + throw new Error("Failed to format as expeceted"); +} From d7bda1f5d93c09033e92296ba1d7b29a6d46cf9b Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Wed, 12 May 2021 13:27:49 -0700 Subject: [PATCH 27/47] Fix issue launching adl-server on Mac OS (#547) We were clearing environment variables down to one environment variable rather than adding one for node options. --- packages/adl-vscode/src/extension.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/adl-vscode/src/extension.ts b/packages/adl-vscode/src/extension.ts index 546f93b39..646308f98 100644 --- a/packages/adl-vscode/src/extension.ts +++ b/packages/adl-vscode/src/extension.ts @@ -1,5 +1,10 @@ import { ExtensionContext, workspace } from "vscode"; -import { Executable, LanguageClient, LanguageClientOptions } from "vscode-languageclient/node.js"; +import { + Executable, + ExecutableOptions, + LanguageClient, + LanguageClientOptions, +} from "vscode-languageclient/node.js"; let client: LanguageClient | undefined; @@ -50,7 +55,14 @@ function resolveADLServer(context: ExtensionContext): Executable { command += ".cmd"; } - return { command, args, options: { env: { NODE_OPTIONS: nodeOptions } } }; + let options: ExecutableOptions | undefined; + if (nodeOptions) { + options = { + env: { ...process.env, NODE_OPTIONS: nodeOptions }, + }; + } + + return { command, args, options }; } export async function deactivate() { From 96de03678af935a93eb34832b6dc45f092355430 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 13 May 2021 09:42:25 -0700 Subject: [PATCH 28/47] Fix: Formatter not rendering template parameters of models. (#548) --- packages/adl/formatter/print/printer.ts | 25 +++++++++++++------ packages/adl/test/formatter/formatter.ts | 12 +++++++++ .../test/formatter/scenarios/inputs/model.adl | 11 ++++++++ .../formatter/scenarios/outputs/model.adl | 8 ++++++ .../adl/test/formatter/scenarios/scenarios.ts | 4 +++ 5 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 packages/adl/test/formatter/scenarios/inputs/model.adl create mode 100644 packages/adl/test/formatter/scenarios/outputs/model.adl diff --git a/packages/adl/formatter/print/printer.ts b/packages/adl/formatter/print/printer.ts index 3cb2a0212..04728deec 100644 --- a/packages/adl/formatter/print/printer.ts +++ b/packages/adl/formatter/print/printer.ts @@ -104,15 +104,24 @@ export function printAliasStatement( options: ADLPrettierOptions, print: PrettierChildPrint ) { - const node = path.getValue(); const id = path.call(print, "id"); - const template = - node.templateParameters.length === 0 - ? "" - : concat(["<", join(", ", path.map(print, "templateParameters")), ">"]); + const template = printTemplateParameters(path, options, print, "templateParameters"); return concat(["alias ", id, template, " = ", path.call(print, "value"), ";"]); } +function printTemplateParameters( + path: FastPath, + options: ADLPrettierOptions, + print: PrettierChildPrint, + propertyName: keyof T +) { + const value = path.getValue()[propertyName]; + if ((value as any).length === 0) { + return ""; + } + return concat(["<", join(", ", path.map(print, propertyName)), ">"]); +} + export function canAttachComment(node: Node): boolean { const kind = node.kind as SyntaxKind; return Boolean(kind && kind !== SyntaxKind.LineComment && kind !== SyntaxKind.BlockComment); @@ -369,10 +378,12 @@ export function printModelStatement( const heritage = node.heritage.length > 0 ? concat(["extends ", path.map(print, "heritage")[0], " "]) : ""; + const generic = printTemplateParameters(path, options, print, "templateParameters"); return concat([ printDecorators(path, options, print, { tryInline: false }), "model ", id, + generic, " ", heritage, printModelPropertiesBlock(path, options, print), @@ -592,9 +603,7 @@ export function printTypeReference( options: ADLPrettierOptions, print: PrettierChildPrint ): prettier.doc.builders.Doc { - const node = path.getValue(); const type = path.call(print, "target"); - const template = - node.arguments.length === 0 ? "" : concat(["<", join(", ", path.map(print, "arguments")), ">"]); + const template = printTemplateParameters(path, options, print, "arguments"); return concat([type, template]); } diff --git a/packages/adl/test/formatter/formatter.ts b/packages/adl/test/formatter/formatter.ts index 352833ede..5baeeed6c 100644 --- a/packages/adl/test/formatter/formatter.ts +++ b/packages/adl/test/formatter/formatter.ts @@ -121,6 +121,18 @@ model Bar extends Base< model Foo extends Base {} model Bar extends Base {} +`, + }); + }); + + it("format model with generic", () => { + assertFormat({ + code: ` +model Foo < T >{ +} +`, + expected: ` +model Foo {} `, }); }); diff --git a/packages/adl/test/formatter/scenarios/inputs/model.adl b/packages/adl/test/formatter/scenarios/inputs/model.adl new file mode 100644 index 000000000..600e89740 --- /dev/null +++ b/packages/adl/test/formatter/scenarios/inputs/model.adl @@ -0,0 +1,11 @@ +model Empty { +} + +model WithProps { + foo: string; + bar: string +} + +model GenericExtension extends Parent { + +} diff --git a/packages/adl/test/formatter/scenarios/outputs/model.adl b/packages/adl/test/formatter/scenarios/outputs/model.adl new file mode 100644 index 000000000..bec1f667b --- /dev/null +++ b/packages/adl/test/formatter/scenarios/outputs/model.adl @@ -0,0 +1,8 @@ +model Empty {} + +model WithProps { + foo: string; + bar: string; +} + +model GenericExtension extends Parent {} \ No newline at end of file diff --git a/packages/adl/test/formatter/scenarios/scenarios.ts b/packages/adl/test/formatter/scenarios/scenarios.ts index 6fd469614..dfd3b805b 100644 --- a/packages/adl/test/formatter/scenarios/scenarios.ts +++ b/packages/adl/test/formatter/scenarios/scenarios.ts @@ -60,4 +60,8 @@ describe("Format scenarios", () => { it("alias", async () => { await testScenario("alias.adl"); }); + + it("model", async () => { + await testScenario("model.adl"); + }); }); From bf53463205880c399ae2d8a6cab40ddee3b07f21 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 13 May 2021 11:38:31 -0700 Subject: [PATCH 29/47] Remove some unused types (#550) --- packages/adl/compiler/formatter.ts | 2 -- packages/adl/formatter/print/types.ts | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/packages/adl/compiler/formatter.ts b/packages/adl/compiler/formatter.ts index 633ddcd3a..be2a35f08 100644 --- a/packages/adl/compiler/formatter.ts +++ b/packages/adl/compiler/formatter.ts @@ -3,8 +3,6 @@ import glob from "glob"; import prettier from "prettier"; import * as adlPrettierPlugin from "../formatter/index.js"; -export class ADLPrettierPluginNotFound extends Error {} - export async function formatADL(code: string): Promise { const output = prettier.format(code, { parser: "adl", diff --git a/packages/adl/formatter/print/types.ts b/packages/adl/formatter/print/types.ts index b942ce6fa..114b7fc68 100644 --- a/packages/adl/formatter/print/types.ts +++ b/packages/adl/formatter/print/types.ts @@ -8,13 +8,3 @@ export type PrettierChildPrint = (path: FastPath, index?: number) => Doc; export interface DecorableNode { decorators: DecoratorExpressionNode[]; } - -type Union = - | { foo: string } - | { bar: string } - | { bar: string } - | { bar: string } - | { bar: string }; -type Inter = { foo: string } & { bar: string } & { bar: string } & { bar: string } & { - bar: string; -}; From bb82633b6c46cef8d8fa5ade0795b047f0b7f867 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 13 May 2021 15:57:33 -0700 Subject: [PATCH 30/47] Fix: Don't format a file that has parsing errors (#549) --- packages/adl/compiler/cli.ts | 2 +- packages/adl/compiler/formatter.ts | 14 ++++++++++-- packages/adl/formatter/parser.ts | 20 +++++++++++++++-- packages/adl/test/formatter/formatter.ts | 28 +++++++++++++++++------- 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/packages/adl/compiler/cli.ts b/packages/adl/compiler/cli.ts index c4f2c802a..5c25218c7 100644 --- a/packages/adl/compiler/cli.ts +++ b/packages/adl/compiler/cli.ts @@ -327,7 +327,7 @@ async function main() { break; } case "format": - await formatADLFiles(args["include"]!); + await formatADLFiles(args["include"]!, { debug: args.debug }); break; } } diff --git a/packages/adl/compiler/formatter.ts b/packages/adl/compiler/formatter.ts index be2a35f08..edd6f0082 100644 --- a/packages/adl/compiler/formatter.ts +++ b/packages/adl/compiler/formatter.ts @@ -2,6 +2,7 @@ import { readFile, writeFile } from "fs/promises"; import glob from "glob"; import prettier from "prettier"; import * as adlPrettierPlugin from "../formatter/index.js"; +import { PrettierParserError } from "../formatter/parser.js"; export async function formatADL(code: string): Promise { const output = prettier.format(code, { @@ -15,10 +16,19 @@ export async function formatADL(code: string): Promise { * Format all the adl files. * @param patterns List of wildcard pattern searching for adl files. */ -export async function formatADLFiles(patterns: string[]) { +export async function formatADLFiles(patterns: string[], { debug }: { debug?: boolean }) { const files = await findFiles(patterns); for (const file of files) { - await formatADLFile(file); + try { + await formatADLFile(file); + } catch (e) { + if (e instanceof PrettierParserError) { + const details = debug ? e.message : ""; + console.error(`File '${file}' failed to fromat. ${details}`); + } else { + throw e; + } + } } } diff --git a/packages/adl/formatter/parser.ts b/packages/adl/formatter/parser.ts index 42efb01d8..b33a07985 100644 --- a/packages/adl/formatter/parser.ts +++ b/packages/adl/formatter/parser.ts @@ -1,11 +1,27 @@ import { Parser, ParserOptions } from "prettier"; import { parse as adlParse } from "../compiler/parser.js"; -import { ADLScriptNode } from "../compiler/types.js"; +import { ADLScriptNode, Diagnostic } from "../compiler/types.js"; export function parse( text: string, parsers: { [parserName: string]: Parser }, opts: ParserOptions & { parentParser?: string } ): ADLScriptNode { - return adlParse(text, { comments: true }); + const result = adlParse(text, { comments: true }); + const errors = result.parseDiagnostics.filter((x) => x.severity === "error"); + if (errors.length > 0) { + throw new PrettierParserError(errors[0]); + } + return result; +} + +export class PrettierParserError extends Error { + public loc: { start: number; end: number }; + public constructor(public readonly error: Diagnostic) { + super(error.message); + this.loc = { + start: error.pos, + end: error.end, + }; + } } diff --git a/packages/adl/test/formatter/formatter.ts b/packages/adl/test/formatter/formatter.ts index 5baeeed6c..00549351a 100644 --- a/packages/adl/test/formatter/formatter.ts +++ b/packages/adl/test/formatter/formatter.ts @@ -1,4 +1,4 @@ -import { strictEqual } from "assert"; +import { strictEqual, throws } from "assert"; import prettier from "prettier"; import * as plugin from "../../formatter/index.js"; @@ -16,6 +16,12 @@ function assertFormat({ code, expected }: { code: string; expected: string }) { } describe("adl: prettier formatter", () => { + it("throws error if there is a parsing issue", () => { + const code = `namespace this is invalid`; + + throws(() => format(code)); + }); + it("format imports", () => { assertFormat({ code: ` @@ -191,11 +197,11 @@ alias Bar = "one" | "two"; it("format generic alias", () => { assertFormat({ code: ` -alias Foo< A, B> = A | B +alias Foo< A, B> = A | B; alias Bar< A, B> = A | - B + B; `, expected: ` alias Foo = A | B; @@ -317,12 +323,19 @@ enum Bar { assertFormat({ code: ` namespace Foo; - -namespace Foo . Bar; `, expected: ` namespace Foo; +`, + }); + }); + it("format global nested namespace", () => { + assertFormat({ + code: ` +namespace Foo . Bar; +`, + expected: ` namespace Foo.Bar; `, }); @@ -371,8 +384,7 @@ namespace Foo.Bar { assertFormat({ code: ` namespace Foo { - -namespace Foo . Bar { +namespace Bar { op some(): string; } } @@ -381,7 +393,7 @@ op some(): string; `, expected: ` namespace Foo { - namespace Foo.Bar { + namespace Bar { op some(): string; } } From 469b1457f49de5e24576ace814fa6e7a8fb1b701 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 14 May 2021 10:23:32 -0700 Subject: [PATCH 31/47] Formatter: Multiline string (#555) --- packages/adl/formatter/print/printer.ts | 34 +++++++- packages/adl/test/formatter/formatter.ts | 104 +++++++++++++++++++++++ 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/packages/adl/formatter/print/printer.ts b/packages/adl/formatter/print/printer.ts index 04728deec..c1bbd43b2 100644 --- a/packages/adl/formatter/print/printer.ts +++ b/packages/adl/formatter/print/printer.ts @@ -14,9 +14,12 @@ import { ModelStatementNode, NamespaceStatementNode, Node, + NumericLiteralNode, OperationStatementNode, Statement, + StringLiteralNode, SyntaxKind, + TextRange, TypeReferenceNode, UnionExpressionNode, } from "../../compiler/types.js"; @@ -77,9 +80,9 @@ export function printADL( case SyntaxKind.Identifier: return node.sv; case SyntaxKind.StringLiteral: - return `"${node.value}"`; + return printStringLiteral(path as FastPath, options); case SyntaxKind.NumericLiteral: - return `${node.value}`; + return printNumberLiteral(path as FastPath, options); case SyntaxKind.ModelExpression: return printModelExpression(path as FastPath, options, print); case SyntaxKind.ModelProperty: @@ -95,7 +98,7 @@ export function printADL( case SyntaxKind.TypeReference: return printTypeReference(path as FastPath, options, print); default: - return options.originalText.slice(node.pos, node.end); + return getRawText(node, options); } } @@ -607,3 +610,28 @@ export function printTypeReference( const template = printTemplateParameters(path, options, print, "arguments"); return concat([type, template]); } + +export function printStringLiteral( + path: prettier.FastPath, + options: ADLPrettierOptions +): prettier.doc.builders.Doc { + const node = path.getValue(); + return getRawText(node, options); +} + +export function printNumberLiteral( + path: prettier.FastPath, + options: ADLPrettierOptions +): prettier.doc.builders.Doc { + const node = path.getValue(); + return getRawText(node, options); +} + +/** + * @param node Node that has postition information. + * @param options Prettier options + * @returns Raw text in the file for the given node. + */ +function getRawText(node: TextRange, options: ADLPrettierOptions) { + return options.originalText.slice(node.pos, node.end); +} diff --git a/packages/adl/test/formatter/formatter.ts b/packages/adl/test/formatter/formatter.ts index 00549351a..7dfafe1b2 100644 --- a/packages/adl/test/formatter/formatter.ts +++ b/packages/adl/test/formatter/formatter.ts @@ -397,6 +397,110 @@ namespace Foo { op some(): string; } } +`, + }); + }); + }); + + describe("string literals", () => { + it("format single line string literal", () => { + assertFormat({ + code: ` +@doc( "this is a doc. " + ) +model Foo {} +`, + expected: ` +@doc("this is a doc. ") +model Foo {} +`, + }); + }); + + it("format single line with newline characters", () => { + assertFormat({ + code: ` +@doc( "foo\\nbar" + ) +model Foo {} +`, + expected: ` +@doc("foo\\nbar") +model Foo {} +`, + }); + }); + + it("format multi line string literal", () => { + assertFormat({ + code: ` +@doc( """ + +this is a doc. + that + span + multiple lines. +""" + ) +model Foo {} +`, + expected: ` +@doc(""" + +this is a doc. + that + span + multiple lines. +""") +model Foo {} +`, + }); + }); + }); + + describe("number literals", () => { + it("format integer", () => { + assertFormat({ + code: ` +alias MyNum = 123 ; +`, + expected: ` +alias MyNum = 123; +`, + }); + }); + + it("format float", () => { + assertFormat({ + code: ` +alias MyFloat1 = 1.234 ; +alias MyFloat2 = 0.123 ; +`, + expected: ` +alias MyFloat1 = 1.234; +alias MyFloat2 = 0.123; +`, + }); + }); + + it("format e notation numbers", () => { + assertFormat({ + code: ` +alias MyBigNumber = 1.0e8 ; +`, + expected: ` +alias MyBigNumber = 1.0e8; +`, + }); + }); + + it("format big numbers", () => { + assertFormat({ + code: ` +alias MyBigNumber = 1.0e999999999 ; +`, + expected: ` +alias MyBigNumber = 1.0e999999999; `, }); }); From cc90a5b228cb935d32c92c82a5498d5d2e4886a1 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Tue, 18 May 2021 09:21:33 -0700 Subject: [PATCH 32/47] Don't fallthrough from `adl vs` to `adl format` (#558) --- packages/adl/compiler/cli.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/adl/compiler/cli.ts b/packages/adl/compiler/cli.ts index 5c25218c7..2ba090ce6 100644 --- a/packages/adl/compiler/cli.ts +++ b/packages/adl/compiler/cli.ts @@ -326,6 +326,7 @@ async function main() { await uninstallVSExtension(); break; } + break; case "format": await formatADLFiles(args["include"]!, { debug: args.debug }); break; From 8ab8f8a6c3f4396a13a10614f8dbee74bc35132d Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Tue, 18 May 2021 14:22:37 -0700 Subject: [PATCH 33/47] Prefer local install of adl package when running global `adl` (#552) Prevents conflicts between two copies of adl modules in global and local package locations. --- packages/adl-vscode/src/extension.ts | 4 +-- packages/adl/cmd/adl-server.js | 3 +- packages/adl/cmd/adl.js | 3 +- packages/adl/cmd/runner.ts | 43 ++++++++++++++++++++++++++++ packages/adl/compiler/cli.ts | 4 +++ packages/adl/server/server.ts | 4 ++- 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 packages/adl/cmd/runner.ts diff --git a/packages/adl-vscode/src/extension.ts b/packages/adl-vscode/src/extension.ts index 646308f98..629c47a98 100644 --- a/packages/adl-vscode/src/extension.ts +++ b/packages/adl-vscode/src/extension.ts @@ -24,9 +24,9 @@ function resolveADLServer(context: ExtensionContext): Executable { const nodeOptions = process.env.ADL_SERVER_NODE_OPTIONS; const args = ["--stdio"]; - // In development mode (F5 launch from source), resolve to locally built adl-server.js. + // In development mode (F5 launch from source), resolve to locally built server.js. if (process.env.ADL_DEVELOPMENT_MODE) { - const script = context.asAbsolutePath("../adl/cmd/adl-server.js"); + const script = context.asAbsolutePath("../adl/dist/server/server.js"); // we use CLI instead of NODE_OPTIONS environment variable in this case // because --nolazy is not supported by NODE_OPTIONS. const options = nodeOptions?.split(" ") ?? []; diff --git a/packages/adl/cmd/adl-server.js b/packages/adl/cmd/adl-server.js index 693261de3..e36c20044 100755 --- a/packages/adl/cmd/adl-server.js +++ b/packages/adl/cmd/adl-server.js @@ -1,2 +1,3 @@ #!/usr/bin/env node -await import("../dist/server/server.js"); +import { runScript } from "../dist/cmd/runner.js"; +await runScript("dist/server/server.js"); diff --git a/packages/adl/cmd/adl.js b/packages/adl/cmd/adl.js index a2286787f..115518b98 100755 --- a/packages/adl/cmd/adl.js +++ b/packages/adl/cmd/adl.js @@ -1,2 +1,3 @@ #!/usr/bin/env node -await import("../dist/compiler/cli.js"); +import { runScript } from "../dist/cmd/runner.js"; +await runScript("dist/compiler/cli.js"); diff --git a/packages/adl/cmd/runner.ts b/packages/adl/cmd/runner.ts new file mode 100644 index 000000000..fae0a652e --- /dev/null +++ b/packages/adl/cmd/runner.ts @@ -0,0 +1,43 @@ +import path from "path"; +import resolveModule from "resolve"; +import url from "url"; + +/** + * Run script given by relative path from @azure-tools/adl package root. + * Prefer local install resolved from cwd over current package. + * + * Prevents loading two conflicting copies of ADL modules from global and + * local package locations. + */ +export function runScript(relativePath: string): Promise { + return new Promise((resolve, reject) => { + resolveModule( + "@azure-tools/adl", + { + basedir: process.cwd(), + preserveSymlinks: false, + }, + (err, resolved) => { + let packageRoot: string; + if (err) { + if ((err as any).code === "MODULE_NOT_FOUND") { + // Resolution from cwd failed: use current package. + packageRoot = path.resolve(url.fileURLToPath(import.meta.url), "../../.."); + } else { + reject(err); + return; + } + } else if (!resolved) { + reject(new Error("BUG: Module resolution succeeded, but didn't return a value.")); + return; + } else { + // Resolution succeeded to dist/compiler/index.js in local package. + packageRoot = path.resolve(resolved, "../../.."); + } + const script = path.join(packageRoot, relativePath); + const scriptUrl = url.pathToFileURL(script).toString(); + resolve(import(scriptUrl)); + } + ); + }); +} diff --git a/packages/adl/compiler/cli.ts b/packages/adl/compiler/cli.ts index 2ba090ce6..b2f19baf5 100644 --- a/packages/adl/compiler/cli.ts +++ b/packages/adl/compiler/cli.ts @@ -97,6 +97,7 @@ const args = yargs(process.argv.slice(2)) array: true, }); }) + .command("info", "Show information about current ADL compiler.") .option("debug", { type: "boolean", description: "Output debug log messages.", @@ -294,6 +295,9 @@ async function main() { let action: string | number; switch (command) { + case "info": + console.log(`Module: ${url.fileURLToPath(import.meta.url)}`); + break; case "compile": options = await getCompilerOptions(); await compileInput(options); diff --git a/packages/adl/server/server.ts b/packages/adl/server/server.ts index db8baf16d..eb88da368 100644 --- a/packages/adl/server/server.ts +++ b/packages/adl/server/server.ts @@ -1,3 +1,4 @@ +import { fileURLToPath } from "url"; import { TextDocument } from "vscode-languageserver-textdocument"; import { Connection, @@ -21,7 +22,8 @@ let documents: TextDocuments; main(); function main() { - log(`** ADL Language Server v${adlVersion} **`); + log(`ADL language server v${adlVersion}\n`); + log(`Module: ${fileURLToPath(import.meta.url)}`); log(`Command Line: ${JSON.stringify(process.argv)}`); connection = createConnection(ProposedFeatures.all); From 9427591bfe70f98e87bba3b6c5c3c1e161c8b509 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Tue, 18 May 2021 14:32:50 -0700 Subject: [PATCH 34/47] Work around npm 7+ Mac OS bug in `adl code install` (#557) --- packages/adl/compiler/cli.ts | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/adl/compiler/cli.ts b/packages/adl/compiler/cli.ts index b2f19baf5..a3d77218a 100644 --- a/packages/adl/compiler/cli.ts +++ b/packages/adl/compiler/cli.ts @@ -1,4 +1,4 @@ -import { spawnSync, SpawnSyncOptionsWithBufferEncoding } from "child_process"; +import { spawnSync, SpawnSyncOptions } from "child_process"; import { mkdtemp, readdir, rmdir } from "fs/promises"; import mkdirp from "mkdirp"; import os from "os"; @@ -181,7 +181,20 @@ async function generateClient(options: CompilerOptions) { async function installVsix(pkg: string, install: (vsixPath: string) => void) { // download npm package to temporary directory const temp = await mkdtemp(path.join(os.tmpdir(), "adl")); - run("npm", ["install", "--silent", "--prefix", temp, pkg]); + const npmArgs = ["install"]; + + // hide npm output unless --debug was passed to adl + if (!args.debug) { + npmArgs.push("--silent"); + } + + // NOTE: Using cwd=temp with `--prefix .` instead of `--prefix ${temp}` to + // workaround https://github.com/npm/cli/issues/3256. It's still important + // to pass --prefix even though we're using cwd as otherwise, npm might + // find a package.json file in a parent directory and install to that + // directory. + npmArgs.push("--prefix", ".", pkg); + run("npm", npmArgs, { cwd: temp }); // locate .vsix const dir = path.join(temp, "node_modules", pkg); @@ -206,7 +219,7 @@ async function installVsix(pkg: string, install: (vsixPath: string) => void) { async function runCode(codeArgs: string[]) { await run(args.insiders ? "code-insiders" : "code", codeArgs, { // VS Code's CLI emits node warnings that we can't do anything about. Suppress them. - env: { ...process.env, NODE_NO_WARNINGS: "1" }, + extraEnv: { NODE_NO_WARNINGS: "1" }, }); } @@ -249,11 +262,25 @@ async function uninstallVSExtension() { // ENOENT checking and handles spaces poorly in some cases. const isCmdOnWindows = ["code", "code-insiders", "npm"]; -function run(command: string, commandArgs: string[], options?: SpawnSyncOptionsWithBufferEncoding) { +interface RunOptions extends SpawnSyncOptions { + extraEnv?: NodeJS.ProcessEnv; +} + +function run(command: string, commandArgs: string[], options?: RunOptions) { if (args.debug) { + if (options) { + console.log(options); + } console.log(`> ${command} ${commandArgs.join(" ")}\n`); } + if (options?.extraEnv) { + options.env = { + ...(options?.env ?? process.env), + ...options.extraEnv, + }; + } + const baseCommandName = path.basename(command); if (process.platform === "win32" && isCmdOnWindows.includes(command)) { command += ".cmd"; From 1a34119e6537d0034e77b10686cfb884e7c0b685 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 18 May 2021 14:58:52 -0700 Subject: [PATCH 35/47] Throw diagnostic error when main adl file is not found. (#560) --- packages/adl/compiler/diagnostics.ts | 35 +++++++++++++++++----------- packages/adl/compiler/messages.ts | 6 +++++ packages/adl/compiler/program.ts | 17 +++++++++++--- packages/adl/compiler/types.ts | 2 +- packages/adl/formatter/parser.ts | 4 ++-- packages/adl/server/server.ts | 4 ++-- 6 files changed, 46 insertions(+), 22 deletions(-) diff --git a/packages/adl/compiler/diagnostics.ts b/packages/adl/compiler/diagnostics.ts index e1df47b53..03adaebe4 100644 --- a/packages/adl/compiler/diagnostics.ts +++ b/packages/adl/compiler/diagnostics.ts @@ -32,6 +32,7 @@ export class AggregateError extends Error { } } +export const NoTarget = Symbol("NoTarget"); export type DiagnosticTarget = Node | Type | Sym | SourceLocation; export type WriteLine = (text?: string) => void; @@ -45,7 +46,7 @@ export const throwOnError: ErrorHandler = throwDiagnostic; export function throwDiagnostic( message: Message | string, - target: DiagnosticTarget, + target: DiagnosticTarget | typeof NoTarget, args?: (string | number)[] ): never { throw new DiagnosticError([createDiagnostic(message, target, args)]); @@ -53,17 +54,18 @@ export function throwDiagnostic( export function createDiagnostic( message: Message | string, - target: DiagnosticTarget, + target: DiagnosticTarget | typeof NoTarget, args?: (string | number)[] ): Diagnostic { - let location: SourceLocation; + let location: Partial = {}; let locationError: Error | undefined; - - try { - location = getSourceLocation(target); - } catch (err) { - locationError = err; - location = createDummySourceLocation(); + if (target !== NoTarget) { + try { + location = getSourceLocation(target); + } catch (err) { + locationError = err; + location = createDummySourceLocation(); + } } if (typeof message === "string") { @@ -94,12 +96,17 @@ export function logDiagnostics(diagnostics: readonly Diagnostic[], writeLine: Wr export function formatDiagnostic(diagnostic: Diagnostic) { const code = diagnostic.code ? ` ADL${diagnostic.code}` : ""; - const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.pos); - const line = pos.line + 1; - const col = pos.character + 1; const severity = diagnostic.severity; - const path = diagnostic.file.path; - return `${path}:${line}:${col} - ${severity}${code}: ${diagnostic.message}`; + const content = `${severity}${code}: ${diagnostic.message}`; + if (diagnostic.file) { + const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.pos ?? 0); + const line = pos.line + 1; + const col = pos.character + 1; + const path = diagnostic.file.path; + return `${path}:${line}:${col} - ${content}`; + } else { + return content; + } } export function createSourceFile(text: string, path: string): SourceFile { diff --git a/packages/adl/compiler/messages.ts b/packages/adl/compiler/messages.ts index b561d711c..5b0f23b4f 100644 --- a/packages/adl/compiler/messages.ts +++ b/packages/adl/compiler/messages.ts @@ -59,6 +59,12 @@ export const Message = { severity: "error", text: "Invalid character.", } as const, + + FileNotFound: { + code: 1109, + text: `File {0} is not found.`, + severity: "error", + } as const, }; // Static assert: this won't compile if one of the entries above is invalid. diff --git a/packages/adl/compiler/program.ts b/packages/adl/compiler/program.ts index 0ab62dbcb..839d2075c 100644 --- a/packages/adl/compiler/program.ts +++ b/packages/adl/compiler/program.ts @@ -2,7 +2,8 @@ import { dirname, extname, isAbsolute, join, resolve } from "path"; import resolveModule from "resolve"; import { createBinder, createSymbolTable } from "./binder.js"; import { createChecker } from "./checker.js"; -import { createSourceFile, DiagnosticError, throwDiagnostic } from "./diagnostics.js"; +import { createSourceFile, DiagnosticError, NoTarget, throwDiagnostic } from "./diagnostics.js"; +import { Message } from "./messages.js"; import { CompilerOptions } from "./options.js"; import { parse } from "./parser.js"; import { @@ -349,8 +350,7 @@ export async function createProgram( const mainPath = resolve(host.getCwd(), options.mainFile); - const mainStat = await host.stat(mainPath); - + const mainStat = await getMainPathStats(mainPath); if (mainStat.isDirectory()) { await loadDirectory(mainPath); } else { @@ -358,6 +358,17 @@ export async function createProgram( } } + async function getMainPathStats(mainPath: string) { + try { + return await host.stat(mainPath); + } catch (e) { + if (e.code === "ENOENT") { + throwDiagnostic(Message.FileNotFound, NoTarget, [mainPath]); + } + throw e; + } + } + function getOption(key: string): string | undefined { return (options.miscOptions || {})[key]; } diff --git a/packages/adl/compiler/types.ts b/packages/adl/compiler/types.ts index 28ab22265..6f58da30c 100644 --- a/packages/adl/compiler/types.ts +++ b/packages/adl/compiler/types.ts @@ -507,7 +507,7 @@ export interface SourceLocation extends TextRange { file: SourceFile; } -export interface Diagnostic extends SourceLocation { +export interface Diagnostic extends Partial { message: string; code?: number; severity: "warning" | "error"; diff --git a/packages/adl/formatter/parser.ts b/packages/adl/formatter/parser.ts index b33a07985..9ed80591a 100644 --- a/packages/adl/formatter/parser.ts +++ b/packages/adl/formatter/parser.ts @@ -20,8 +20,8 @@ export class PrettierParserError extends Error { public constructor(public readonly error: Diagnostic) { super(error.message); this.loc = { - start: error.pos, - end: error.end, + start: error.pos ?? 0, + end: error.end ?? 0, }; } } diff --git a/packages/adl/server/server.ts b/packages/adl/server/server.ts index eb88da368..121bec3b8 100644 --- a/packages/adl/server/server.ts +++ b/packages/adl/server/server.ts @@ -50,8 +50,8 @@ function checkChange(change: TextDocumentChangeEvent) { const diagnostics: Diagnostic[] = []; for (const each of parseDiagnostics) { - const start = document.positionAt(each.pos); - const end = document.positionAt(each.end); + const start = document.positionAt(each.pos ?? 0); + const end = document.positionAt(each.end ?? 0); const range = Range.create(start, end); const severity = convertSeverity(each.severity); const diagnostic = Diagnostic.create(range, each.message, severity, each.code, "ADL"); From 0ce71bfb7eb90dc7c4bddf13af43e97a975ca867 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Tue, 18 May 2021 17:58:38 -0700 Subject: [PATCH 36/47] Prepare publish of v0.11 (#561) --- packages/adl-rest/CHANGELOG.json | 12 +++++++ packages/adl-rest/CHANGELOG.md | 7 ++++- packages/adl-rest/package.json | 4 +-- packages/adl-vs/CHANGELOG.json | 12 +++++++ packages/adl-vs/CHANGELOG.md | 7 ++++- packages/adl-vs/package.json | 4 +-- packages/adl-vscode/CHANGELOG.json | 12 +++++++ packages/adl-vscode/CHANGELOG.md | 9 +++++- packages/adl-vscode/package.json | 2 +- packages/adl/CHANGELOG.json | 35 +++++++++++++++++++++ packages/adl/CHANGELOG.md | 19 ++++++++++- packages/adl/package.json | 2 +- packages/prettier-plugin-adl/CHANGELOG.json | 17 ++++++++++ packages/prettier-plugin-adl/CHANGELOG.md | 9 ++++++ packages/prettier-plugin-adl/package.json | 4 +-- 15 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 packages/prettier-plugin-adl/CHANGELOG.json create mode 100644 packages/prettier-plugin-adl/CHANGELOG.md diff --git a/packages/adl-rest/CHANGELOG.json b/packages/adl-rest/CHANGELOG.json index d5d9c0187..4d76da126 100644 --- a/packages/adl-rest/CHANGELOG.json +++ b/packages/adl-rest/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@azure-tools/adl-rest", "entries": [ + { + "version": "0.2.1", + "tag": "@azure-tools/adl-rest_v0.2.1", + "date": "Tue, 18 May 2021 23:43:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@azure-tools/adl\" from `0.10.0` to `0.11.0`" + } + ] + } + }, { "version": "0.2.0", "tag": "@azure-tools/adl-rest_v0.2.0", diff --git a/packages/adl-rest/CHANGELOG.md b/packages/adl-rest/CHANGELOG.md index a9b5412c5..d1a5970cd 100644 --- a/packages/adl-rest/CHANGELOG.md +++ b/packages/adl-rest/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @azure-tools/adl-rest -This log was last generated on Thu, 06 May 2021 14:56:01 GMT and should not be manually modified. +This log was last generated on Tue, 18 May 2021 23:43:31 GMT and should not be manually modified. + +## 0.2.1 +Tue, 18 May 2021 23:43:31 GMT + +_Version update only_ ## 0.2.0 Thu, 06 May 2021 14:56:01 GMT diff --git a/packages/adl-rest/package.json b/packages/adl-rest/package.json index 94908ed53..d4bc3d8ac 100644 --- a/packages/adl-rest/package.json +++ b/packages/adl-rest/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/adl-rest", - "version": "0.2.0", + "version": "0.2.1", "author": "Microsoft Corporation", "description": "ADL REST protocol binding", "homepage": "https://github.com/Azure/adl", @@ -32,7 +32,7 @@ "!dist/test/**" ], "dependencies": { - "@azure-tools/adl": "0.10.0" + "@azure-tools/adl": "0.11.0" }, "devDependencies": { "@types/node": "~14.0.27", diff --git a/packages/adl-vs/CHANGELOG.json b/packages/adl-vs/CHANGELOG.json index 94ed454e5..d7312cdb6 100644 --- a/packages/adl-vs/CHANGELOG.json +++ b/packages/adl-vs/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@azure-tools/adl-vs", "entries": [ + { + "version": "0.1.5", + "tag": "@azure-tools/adl-vs_v0.1.5", + "date": "Tue, 18 May 2021 23:43:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"adl-vscode\" from `0.5.0` to `0.5.1`" + } + ] + } + }, { "version": "0.1.4", "tag": "@azure-tools/adl-vs_v0.1.4", diff --git a/packages/adl-vs/CHANGELOG.md b/packages/adl-vs/CHANGELOG.md index bbd853037..fc1ba8160 100644 --- a/packages/adl-vs/CHANGELOG.md +++ b/packages/adl-vs/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @azure-tools/adl-vs -This log was last generated on Thu, 06 May 2021 14:56:02 GMT and should not be manually modified. +This log was last generated on Tue, 18 May 2021 23:43:31 GMT and should not be manually modified. + +## 0.1.5 +Tue, 18 May 2021 23:43:31 GMT + +_Version update only_ ## 0.1.4 Thu, 06 May 2021 14:56:02 GMT diff --git a/packages/adl-vs/package.json b/packages/adl-vs/package.json index 3e8adc87e..59e684774 100644 --- a/packages/adl-vs/package.json +++ b/packages/adl-vs/package.json @@ -1,7 +1,7 @@ { "name": "@azure-tools/adl-vs", "author": "Microsoft Corporation", - "version": "0.1.4", + "version": "0.1.5", "description": "ADL Language Support for Visual Studio", "homepage": "https://github.com/Azure/adl", "readme": "https://github.com/Azure/adl/blob/master/README.md", @@ -28,6 +28,6 @@ }, "dependencies": {}, "devDependencies": { - "adl-vscode": "0.5.0" + "adl-vscode": "0.5.1" } } diff --git a/packages/adl-vscode/CHANGELOG.json b/packages/adl-vscode/CHANGELOG.json index 5a316b555..465ac2d8e 100644 --- a/packages/adl-vscode/CHANGELOG.json +++ b/packages/adl-vscode/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "adl-vscode", "entries": [ + { + "version": "0.5.1", + "tag": "adl-vscode_v0.5.1", + "date": "Tue, 18 May 2021 23:43:31 GMT", + "comments": { + "patch": [ + { + "comment": "Fix issue launching adl-server on Mac OS" + } + ] + } + }, { "version": "0.5.0", "tag": "adl-vscode_v0.5.0", diff --git a/packages/adl-vscode/CHANGELOG.md b/packages/adl-vscode/CHANGELOG.md index 48854f035..4cbd5889f 100644 --- a/packages/adl-vscode/CHANGELOG.md +++ b/packages/adl-vscode/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - adl-vscode -This log was last generated on Thu, 06 May 2021 14:56:02 GMT and should not be manually modified. +This log was last generated on Tue, 18 May 2021 23:43:31 GMT and should not be manually modified. + +## 0.5.1 +Tue, 18 May 2021 23:43:31 GMT + +### Patches + +- Fix issue launching adl-server on Mac OS ## 0.5.0 Thu, 06 May 2021 14:56:02 GMT diff --git a/packages/adl-vscode/package.json b/packages/adl-vscode/package.json index 7fc32d3a7..9b66527b3 100644 --- a/packages/adl-vscode/package.json +++ b/packages/adl-vscode/package.json @@ -1,6 +1,6 @@ { "name": "adl-vscode", - "version": "0.5.0", + "version": "0.5.1", "author": "Microsoft Corporation", "description": "ADL Language Support for VS Code", "homepage": "https://github.com/Azure/adl", diff --git a/packages/adl/CHANGELOG.json b/packages/adl/CHANGELOG.json index 30ba7d140..20220107a 100644 --- a/packages/adl/CHANGELOG.json +++ b/packages/adl/CHANGELOG.json @@ -1,6 +1,41 @@ { "name": "@azure-tools/adl", "entries": [ + { + "version": "0.11.0", + "tag": "@azure-tools/adl_v0.11.0", + "date": "Tue, 18 May 2021 23:43:31 GMT", + "comments": { + "patch": [ + { + "comment": "**Fix** Throw diagnostic when main ADL file doesn't exists" + }, + { + "comment": "Fix TypeError after `adl vs` command" + }, + { + "comment": "**Fix** formatter handling string and number literal should keep as it is." + }, + { + "comment": "**Fix** Formatter not rendering template parameters of models." + }, + { + "comment": "**Fix** Don't format a file with parsing errors" + }, + { + "comment": "Work around npm 7+ Mac OS bug in `adl code install`" + }, + { + "comment": "Prefer local install of adl package when running global `adl`" + } + ], + "minor": [ + { + "comment": "**Added** format command to automatically format adl files" + } + ] + } + }, { "version": "0.10.0", "tag": "@azure-tools/adl_v0.10.0", diff --git a/packages/adl/CHANGELOG.md b/packages/adl/CHANGELOG.md index e69366772..013c7223b 100644 --- a/packages/adl/CHANGELOG.md +++ b/packages/adl/CHANGELOG.md @@ -1,6 +1,23 @@ # Change Log - @azure-tools/adl -This log was last generated on Thu, 06 May 2021 14:56:02 GMT and should not be manually modified. +This log was last generated on Tue, 18 May 2021 23:43:31 GMT and should not be manually modified. + +## 0.11.0 +Tue, 18 May 2021 23:43:31 GMT + +### Minor changes + +- **Added** format command to automatically format adl files + +### Patches + +- **Fix** Throw diagnostic when main ADL file doesn't exists +- Fix TypeError after `adl vs` command +- **Fix** formatter handling string and number literal should keep as it is. +- **Fix** Formatter not rendering template parameters of models. +- **Fix** Don't format a file with parsing errors +- Work around npm 7+ Mac OS bug in `adl code install` +- Prefer local install of adl package when running global `adl` ## 0.10.0 Thu, 06 May 2021 14:56:02 GMT diff --git a/packages/adl/package.json b/packages/adl/package.json index 7949067ef..ee5b89fb9 100644 --- a/packages/adl/package.json +++ b/packages/adl/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/adl", - "version": "0.10.0", + "version": "0.11.0", "description": "ADL Compiler Preview", "author": "Microsoft Corporation", "license": "MIT", diff --git a/packages/prettier-plugin-adl/CHANGELOG.json b/packages/prettier-plugin-adl/CHANGELOG.json new file mode 100644 index 000000000..24558c1f3 --- /dev/null +++ b/packages/prettier-plugin-adl/CHANGELOG.json @@ -0,0 +1,17 @@ +{ + "name": "@azure-tools/prettier-plugin-adl", + "entries": [ + { + "version": "0.1.1", + "tag": "@azure-tools/prettier-plugin-adl_v0.1.1", + "date": "Tue, 18 May 2021 23:43:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@azure-tools/adl\" from `0.10.0` to `0.11.0`" + } + ] + } + } + ] +} diff --git a/packages/prettier-plugin-adl/CHANGELOG.md b/packages/prettier-plugin-adl/CHANGELOG.md new file mode 100644 index 000000000..2abda2bf7 --- /dev/null +++ b/packages/prettier-plugin-adl/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log - @azure-tools/prettier-plugin-adl + +This log was last generated on Tue, 18 May 2021 23:43:31 GMT and should not be manually modified. + +## 0.1.1 +Tue, 18 May 2021 23:43:31 GMT + +_Initial release_ + diff --git a/packages/prettier-plugin-adl/package.json b/packages/prettier-plugin-adl/package.json index c85897c5f..cb00ff9fa 100644 --- a/packages/prettier-plugin-adl/package.json +++ b/packages/prettier-plugin-adl/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/prettier-plugin-adl", - "version": "0.1.0", + "version": "0.1.1", "description": "", "main": "dist/index.js", "scripts": { @@ -14,7 +14,7 @@ "prettier": "~2.2.1" }, "devDependencies": { - "@azure-tools/adl": "0.10.0", + "@azure-tools/adl": "0.11.0", "@rollup/plugin-commonjs": "~17.1.0", "@rollup/plugin-json": "~4.1.0", "@rollup/plugin-node-resolve": "~11.2.0", From 62da05403d049eeb5169b21d278ba9356c37e29b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 27 May 2021 08:33:39 -0700 Subject: [PATCH 37/47] User Configuration file discovery and loading (#562) --- packages/adl/compiler/cli.ts | 29 +++- packages/adl/compiler/messages.ts | 6 + packages/adl/config/config-loader.ts | 143 ++++++++++++++++++ packages/adl/config/config-schema.ts | 46 ++++++ packages/adl/config/config-validator.ts | 52 +++++++ packages/adl/config/index.ts | 2 + packages/adl/config/types.ts | 36 +++++ packages/adl/package.json | 3 + packages/adl/test/config/config.ts | 95 ++++++++++++ .../adl/test/config/scenarios/empty/.keep | 0 .../test/config/scenarios/json/.adlrc.json | 6 + .../scenarios/package-json/package.json | 10 ++ .../config/scenarios/yaml-json/.adlrc.yaml | 6 + .../test/config/scenarios/yaml/.adlrc.json | 6 + .../test/config/scenarios/yaml/.adlrc.yaml | 6 + 15 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 packages/adl/config/config-loader.ts create mode 100644 packages/adl/config/config-schema.ts create mode 100644 packages/adl/config/config-validator.ts create mode 100644 packages/adl/config/index.ts create mode 100644 packages/adl/config/types.ts create mode 100644 packages/adl/test/config/config.ts create mode 100644 packages/adl/test/config/scenarios/empty/.keep create mode 100644 packages/adl/test/config/scenarios/json/.adlrc.json create mode 100644 packages/adl/test/config/scenarios/package-json/package.json create mode 100644 packages/adl/test/config/scenarios/yaml-json/.adlrc.yaml create mode 100644 packages/adl/test/config/scenarios/yaml/.adlrc.json create mode 100644 packages/adl/test/config/scenarios/yaml/.adlrc.yaml diff --git a/packages/adl/compiler/cli.ts b/packages/adl/compiler/cli.ts index a3d77218a..ad493ef3b 100644 --- a/packages/adl/compiler/cli.ts +++ b/packages/adl/compiler/cli.ts @@ -7,6 +7,7 @@ import url from "url"; import yargs from "yargs"; import { CompilerOptions } from "../compiler/options.js"; import { compile } from "../compiler/program.js"; +import { loadADLConfigInDir } from "../config/index.js"; import { compilerAssert, DiagnosticError, dumpError, logDiagnostics } from "./diagnostics.js"; import { formatADLFiles } from "./formatter.js"; import { adlVersion, NodeHost } from "./util.js"; @@ -258,6 +259,32 @@ async function uninstallVSExtension() { run(vsixInstaller, ["/uninstall:88b9492f-c019-492c-8aeb-f325a7e4cf23"]); } +/** + * Print the resolved adl configuration. + */ +async function printInfo() { + const cwd = process.cwd(); + console.log(`Module: ${url.fileURLToPath(import.meta.url)}`); + + try { + const config = await loadADLConfigInDir(cwd); + const jsyaml = await import("js-yaml"); + console.log(`User Config: ${config.filename ?? "No config file found"}`); + console.log("-----------"); + console.log(jsyaml.dump(config)); + console.log("-----------"); + } catch (err) { + if (err instanceof DiagnosticError) { + logDiagnostics(err.diagnostics, console.error); + if (args.debug) { + console.error(`Stack trace:\n\n${err.stack}`); + } + process.exit(1); + } + throw err; // let non-diagnostic errors go to top-level bug handler. + } +} + // NOTE: We could also use { shell: true } to let windows find the .cmd, but that breaks // ENOENT checking and handles spaces poorly in some cases. const isCmdOnWindows = ["code", "code-insiders", "npm"]; @@ -323,7 +350,7 @@ async function main() { switch (command) { case "info": - console.log(`Module: ${url.fileURLToPath(import.meta.url)}`); + printInfo(); break; case "compile": options = await getCompilerOptions(); diff --git a/packages/adl/compiler/messages.ts b/packages/adl/compiler/messages.ts index 5b0f23b4f..133bd4cb3 100644 --- a/packages/adl/compiler/messages.ts +++ b/packages/adl/compiler/messages.ts @@ -65,6 +65,12 @@ export const Message = { text: `File {0} is not found.`, severity: "error", } as const, + + InvalidConfigFormat: { + code: 1110, + text: `File {0} does not have a known configuration format.`, + severity: "error", + } as const, }; // Static assert: this won't compile if one of the entries above is invalid. diff --git a/packages/adl/config/config-loader.ts b/packages/adl/config/config-loader.ts new file mode 100644 index 000000000..ff86f67c6 --- /dev/null +++ b/packages/adl/config/config-loader.ts @@ -0,0 +1,143 @@ +import { access, readFile } from "fs/promises"; +import { basename, extname, join } from "path"; +import { createSourceFile, Message, NoTarget, throwDiagnostic } from "../compiler/diagnostics.js"; +import { ConfigValidator } from "./config-validator.js"; +import { ADLConfig, ADLRawConfig, ConfigFile } from "./types.js"; + +const configFilenames = [".adlrc.yaml", ".adlrc.yml", ".adlrc.json", "package.json"]; + +const defaultConfig: ADLConfig = { + plugins: [], + emitters: {}, + lint: { + extends: [], + rules: {}, + }, +}; + +/** + * Load ADL Configuration if present. + * @param directoryPath Current working directory where the config should be. + */ +export async function loadADLConfigInDir(directoryPath: string): Promise { + for (const filename of configFilenames) { + const filePath = join(directoryPath, filename); + if (await fileExists(filePath)) { + return loadADLConfigFile(filePath); + } + } + return defaultConfig; +} + +/** + * Load given file as an adl configuration + */ +export async function loadADLConfigFile(filePath: string): Promise { + switch (extname(filePath)) { + case ".json": + if (basename(filePath) === "package.json") { + return loadPackageJSONConfigFile(filePath); + } + return loadJSONConfigFile(filePath); + + case ".yaml": + case ".yml": + return loadYAMLConfigFile(filePath); + + default: + throwDiagnostic(Message.InvalidConfigFormat, NoTarget, [filePath]); + } +} + +export async function loadPackageJSONConfigFile(filePath: string): Promise { + const rawConfig = await loadJSON(filePath); + if (rawConfig.data.adl) { + return normalizeConfig({ file: rawConfig.file, data: rawConfig.data.adl }); + } else { + return defaultConfig; + } +} + +export async function loadJSONConfigFile(filePath: string): Promise { + const rawConfig = await loadJSON(filePath); + return normalizeConfig(rawConfig); +} + +/** + * Loads a YAML configuration from a file. + * @param filePath Path to the file. + */ +export async function loadYAMLConfigFile(filePath: string): Promise { + const rawConfig = await loadYaml(filePath); + return normalizeConfig(rawConfig); +} + +/** + * Load YAML and throw @see DiagnosticError if there is an issue. + * @param filePath Yaml file path. + * @returns Parsed object. + */ +async function loadYaml(filePath: string): Promise> { + // Lazy load. + const jsyaml = await import("js-yaml"); + + const content = (await readFile(filePath)).toString(); + const file = createSourceFile(content, filePath); + try { + return { + file, + data: jsyaml.load(content) as ADLRawConfig, + }; + } catch (e) { + throwDiagnostic(e.message, { file, pos: 0, end: 0 }); + } +} + +/** + * Load JSON and throw @see DiagnosticError if there is an issue. + * @param filePath JSON file path. + * @returns Parsed object. + */ +async function loadJSON(filePath: string): Promise> { + const content = (await readFile(filePath)).toString(); + const file = createSourceFile(content, filePath); + + try { + return { + file, + data: JSON.parse(content), + }; + } catch (e) { + throwDiagnostic(e.message, { file, pos: 0, end: 0 }); + } +} + +const configValidator = new ConfigValidator(); +export function normalizeConfig(config: ConfigFile): ADLConfig { + configValidator.validateConfig(config.data, config.file); + return { + filename: config.file.path, + ...defaultConfig, + ...config.data, + lint: { + ...defaultConfig.lint, + ...(config.data.lint ?? {}), + }, + }; +} + +/** + * Validate the given config is valid. + */ +export function validateConfig(config: ADLRawConfig) { + return {} as any; +} + +async function fileExists(path: string) { + try { + await access(path); + return true; + } catch { + return false; + } +} diff --git a/packages/adl/config/config-schema.ts b/packages/adl/config/config-schema.ts new file mode 100644 index 000000000..8f79b47bb --- /dev/null +++ b/packages/adl/config/config-schema.ts @@ -0,0 +1,46 @@ +import { JSONSchemaType } from "ajv"; +import { ADLRawConfig } from "./types.js"; + +export const ADLConfigJsonSchema: JSONSchemaType = { + type: "object", + additionalProperties: false, + properties: { + plugins: { + type: "array", + nullable: true, + items: { + type: "string", + }, + }, + lint: { + type: "object", + nullable: true, + additionalProperties: false, + properties: { + extends: { + type: "array", + nullable: true, + items: { + type: "string", + }, + }, + rules: { + type: "object", + nullable: true, + required: [], + additionalProperties: { + oneOf: [{ type: "string", enum: ["on", "off"] }, { type: "object" }], + }, + }, + }, + }, + emitters: { + type: "object", + nullable: true, + required: [], + additionalProperties: { + type: "boolean", + }, + }, + }, +}; diff --git a/packages/adl/config/config-validator.ts b/packages/adl/config/config-validator.ts new file mode 100644 index 000000000..faaaed81c --- /dev/null +++ b/packages/adl/config/config-validator.ts @@ -0,0 +1,52 @@ +import Ajv, { ErrorObject } from "ajv"; +import { DiagnosticError } from "../compiler/diagnostics.js"; +import { Diagnostic, SourceFile } from "../compiler/types.js"; +import { ADLConfigJsonSchema } from "./config-schema.js"; +import { ADLRawConfig } from "./types.js"; + +export class ConfigValidator { + private ajv = new Ajv({ + strict: true, + }); + + /** + * Validate the config is valid + * @param config Configuration + * @param file @optional file for errors tracing. + * @returns + */ + public validateConfig(config: ADLRawConfig, file?: SourceFile) { + this.validateSchema(config, file); + } + + private validateSchema(config: ADLRawConfig, file?: SourceFile) { + const validate = this.ajv.compile(ADLConfigJsonSchema); + const valid = validate(config); + + if (!valid && validate.errors) { + throw new DiagnosticError( + validate.errors.map((error) => { + return ajvErrorToDiagnostic(error, file); + }) + ); + } + } +} + +const IGNORED_AJV_PARAMS = new Set(["type", "errors"]); + +function ajvErrorToDiagnostic(error: ErrorObject, file?: SourceFile): Diagnostic { + const messageLines = [`Schema violation: ${error.message} (${error.instancePath || "/"})`]; + for (const [name, value] of Object.entries(error.params).filter( + ([name]) => !IGNORED_AJV_PARAMS.has(name) + )) { + const formattedValue = Array.isArray(value) ? [...new Set(value)].join(", ") : value; + messageLines.push(` ${name}: ${formattedValue}`); + } + + return { + severity: "error", + message: messageLines.join("\n"), + ...(file && { file }), + }; +} diff --git a/packages/adl/config/index.ts b/packages/adl/config/index.ts new file mode 100644 index 000000000..f66db80ca --- /dev/null +++ b/packages/adl/config/index.ts @@ -0,0 +1,2 @@ +export * from "./config-loader.js"; +export * from "./types.js"; diff --git a/packages/adl/config/types.ts b/packages/adl/config/types.ts new file mode 100644 index 000000000..e078130fb --- /dev/null +++ b/packages/adl/config/types.ts @@ -0,0 +1,36 @@ +import { SourceFile } from "../compiler"; + +/** + * Represent the normalized user configuration. + */ +export interface ADLConfig { + /** + * Path to the config file used to create this configuration. + */ + filename?: string; + + plugins: string[]; + lint: ADLLintConfig; + emitters: Record; +} + +export type RuleValue = "on" | "off" | {}; + +export interface ADLLintConfig { + extends: string[]; + rules: Record; +} + +/** + * Represent the configuration that can be provided in a config file. + */ +export interface ADLRawConfig { + plugins?: string[]; + lint?: Partial; + emitters?: Record; +} + +export interface ConfigFile { + file: SourceFile; + data: T; +} diff --git a/packages/adl/package.json b/packages/adl/package.json index ee5b89fb9..26837b296 100644 --- a/packages/adl/package.json +++ b/packages/adl/package.json @@ -45,7 +45,9 @@ }, "dependencies": { "autorest": "~3.0.6335", + "ajv": "~8.4.0", "glob": "~7.1.6", + "js-yaml": "~4.1.0", "mkdirp": "~1.0.4", "prettier": "~2.2.1", "resolve": "~1.20.0", @@ -55,6 +57,7 @@ }, "devDependencies": { "@types/glob": "~7.1.3", + "@types/js-yaml": "~4.0.1", "@types/mkdirp": "~1.0.1", "@types/mocha": "~7.0.2", "@types/node": "~14.0.27", diff --git a/packages/adl/test/config/config.ts b/packages/adl/test/config/config.ts new file mode 100644 index 000000000..07b1f4a55 --- /dev/null +++ b/packages/adl/test/config/config.ts @@ -0,0 +1,95 @@ +import { deepStrictEqual, throws } from "assert"; +import { dirname, join, resolve } from "path"; +import { fileURLToPath } from "url"; +import { DiagnosticError } from "../../compiler/diagnostics.js"; +import { ConfigValidator } from "../../config/config-validator.js"; +import { loadADLConfigInDir } from "../../config/index.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +describe("adl: Config file loading", () => { + describe("file discovery", async () => { + const scenarioRoot = resolve(__dirname, "../../../test/config/scenarios"); + const loadTestConfig = async (folderName: string) => { + const folderPath = join(scenarioRoot, folderName); + const { filename, ...config } = await loadADLConfigInDir(folderPath); + return config; + }; + + const assertLoadFromFolder = async (folderName: string) => { + const config = await loadTestConfig(folderName); + deepStrictEqual(config, { + plugins: ["foo"], + emitters: { + "foo:openapi": true, + }, + lint: { + extends: [], + rules: {}, + }, + }); + }; + + it("Loads yaml config file", async () => { + await assertLoadFromFolder("yaml"); + }); + + it("Loads json config file", async () => { + await assertLoadFromFolder("json"); + }); + + it("Loads from adl section in package.json config file", async () => { + await assertLoadFromFolder("package-json"); + }); + + it("Loads empty config if it can't find any config files", async () => { + const config = await loadTestConfig("empty"); + deepStrictEqual(config, { + plugins: [], + emitters: {}, + lint: { + extends: [], + rules: {}, + }, + }); + }); + + it("only loads first config file found", async () => { + // Should load .adlrc.yaml and MOT load .adlrc.json here + await assertLoadFromFolder("yaml-json"); + }); + }); + + describe("validation", () => { + const validator = new ConfigValidator(); + + it("does not allow additional properties", () => { + throws( + () => validator.validateConfig({ someCustomProp: true } as any), + new DiagnosticError([ + { + severity: "error", + message: + "Schema violation: must NOT have additional properties (/)\n additionalProperty: someCustomProp", + }, + ]) + ); + }); + + it("fail if passing the wrong type", () => { + throws( + () => validator.validateConfig({ emitters: true } as any), + new DiagnosticError([ + { + severity: "error", + message: "Schema violation: must be object (/emitters)", + }, + ]) + ); + }); + + it("succeeed if config is valid", () => { + validator.validateConfig({ lint: { rules: { foo: "on" } } }); + }); + }); +}); diff --git a/packages/adl/test/config/scenarios/empty/.keep b/packages/adl/test/config/scenarios/empty/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/adl/test/config/scenarios/json/.adlrc.json b/packages/adl/test/config/scenarios/json/.adlrc.json new file mode 100644 index 000000000..9a302c2d8 --- /dev/null +++ b/packages/adl/test/config/scenarios/json/.adlrc.json @@ -0,0 +1,6 @@ +{ + "plugins": ["foo"], + "emitters": { + "foo:openapi": true + } +} diff --git a/packages/adl/test/config/scenarios/package-json/package.json b/packages/adl/test/config/scenarios/package-json/package.json new file mode 100644 index 000000000..cc2f4f645 --- /dev/null +++ b/packages/adl/test/config/scenarios/package-json/package.json @@ -0,0 +1,10 @@ +{ + "adl": { + "plugins": [ + "foo" + ], + "emitters": { + "foo:openapi": true + } + } +} diff --git a/packages/adl/test/config/scenarios/yaml-json/.adlrc.yaml b/packages/adl/test/config/scenarios/yaml-json/.adlrc.yaml new file mode 100644 index 000000000..90baaf1ea --- /dev/null +++ b/packages/adl/test/config/scenarios/yaml-json/.adlrc.yaml @@ -0,0 +1,6 @@ +plugins: + - foo + +# This has comments +emitters: + foo:openapi: true diff --git a/packages/adl/test/config/scenarios/yaml/.adlrc.json b/packages/adl/test/config/scenarios/yaml/.adlrc.json new file mode 100644 index 000000000..e93e1ffe4 --- /dev/null +++ b/packages/adl/test/config/scenarios/yaml/.adlrc.json @@ -0,0 +1,6 @@ +{ + "plugins": ["otherconfig"], + "emitters": { + "foo:other": true + } +} diff --git a/packages/adl/test/config/scenarios/yaml/.adlrc.yaml b/packages/adl/test/config/scenarios/yaml/.adlrc.yaml new file mode 100644 index 000000000..90baaf1ea --- /dev/null +++ b/packages/adl/test/config/scenarios/yaml/.adlrc.yaml @@ -0,0 +1,6 @@ +plugins: + - foo + +# This has comments +emitters: + foo:openapi: true From d2658c5e3388a6b050732db3233fdd58c6976e53 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 27 May 2021 09:09:14 -0700 Subject: [PATCH 38/47] Run prettier plugin test in CI and mocha explorer, fix test bug (#570) --- eng/scripts/helpers.js | 8 ++++++-- .../adl/test/formatter/scenarios/scenarios.ts | 2 +- packages/prettier-plugin-adl/package.json | 4 +++- packages/prettier-plugin-adl/test/smoke.js | 17 ++++++++++------- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/eng/scripts/helpers.js b/eng/scripts/helpers.js index 46a23f294..9e10c2cab 100644 --- a/eng/scripts/helpers.js +++ b/eng/scripts/helpers.js @@ -1,6 +1,6 @@ import { spawn, spawnSync } from "child_process"; import { statSync, readFileSync } from "fs"; -import { dirname, resolve } from "path"; +import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; function read(filename) { @@ -31,7 +31,11 @@ export function forEachProject(onEach) { export function npmForEach(cmd, options) { forEachProject((name, location, project) => { - // checks for the script first + if (cmd === "test-official" && !project.scripts[cmd] && project.scripts["test"]) { + const pj = join(location, "package.json"); + throw new Error(`${pj} has a 'test' script, but no 'test-official' script for CI.`); + } + if (project.scripts[cmd] || cmd === "pack") { const args = cmd === "pack" ? [cmd] : ["run", cmd]; run("npm", args, { cwd: location, ...options }); diff --git a/packages/adl/test/formatter/scenarios/scenarios.ts b/packages/adl/test/formatter/scenarios/scenarios.ts index dfd3b805b..3d193a024 100644 --- a/packages/adl/test/formatter/scenarios/scenarios.ts +++ b/packages/adl/test/formatter/scenarios/scenarios.ts @@ -52,7 +52,7 @@ async function testScenario(name: string) { } } -describe("Format scenarios", () => { +describe("adl: prettier formatter scenarios", () => { it("misc", async () => { await testScenario("misc.adl"); }); diff --git a/packages/prettier-plugin-adl/package.json b/packages/prettier-plugin-adl/package.json index cb00ff9fa..788d4c186 100644 --- a/packages/prettier-plugin-adl/package.json +++ b/packages/prettier-plugin-adl/package.json @@ -5,7 +5,8 @@ "main": "dist/index.js", "scripts": { "build": "rollup --config 2>&1 && npm run generate-third-party-notices", - "test": "node ./test/smoke.js", + "test": "mocha --timeout 5000 'test/**/*.js'", + "test-official": "mocha --timeout 5000 --forbid-only 'test/**/*.js'", "generate-third-party-notices": "node ../../eng/scripts/generate-third-party-notices" }, "author": "Microsoft Corporation", @@ -19,6 +20,7 @@ "@rollup/plugin-json": "~4.1.0", "@rollup/plugin-node-resolve": "~11.2.0", "@rollup/plugin-replace": "~2.4.2", + "mocha": "~8.3.2", "rollup": "~2.41.4" }, "files": [ diff --git a/packages/prettier-plugin-adl/test/smoke.js b/packages/prettier-plugin-adl/test/smoke.js index 7a24df1e3..46f806117 100644 --- a/packages/prettier-plugin-adl/test/smoke.js +++ b/packages/prettier-plugin-adl/test/smoke.js @@ -1,11 +1,14 @@ // Simple smoke test verifying the plugin is able to be loaded via prettier. const prettier = require("prettier"); +const { strictEqual } = require("assert"); +const { resolve } = require("path"); -const result = prettier.format("alias Foo = string", { - parser: "adl", - plugins: ["."], +describe("prettier-plugin: smoke test", () => { + it("loads and formats", () => { + const result = prettier.format("alias Foo = string;", { + parser: "adl", + plugins: [resolve(__dirname, "..")], + }); + strictEqual(result, "alias Foo = string;"); + }); }); - -if (result !== "alias Foo = string;") { - throw new Error("Failed to format as expeceted"); -} From 451274c2695d8d0e52d1872e071137e2c93994a3 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Wed, 2 Jun 2021 15:20:45 -0700 Subject: [PATCH 39/47] Facilitate merge of config file loading with semantic error recovery (#572) It ended up being tricky to merge my pending semantic error recovery change with the config file loading change. This change extracts out some changes to config file loading from my error recovery branch as it will be easier to review this independently from that. * Don't bother making a diagnostic for incorrect file extension, as compiler should never attempt that. It's OK to throw a regular error in this case. * Fix issue with returning or spreading default config where mutation of loaded config could mutate defaults. * Avoid fileExists + readFile race condition by not using fileExists and catching readFile ENOENT error instead. * Move extra json file for multi-file test to correct folder. * Various small refactorings to make these changes easier. --- packages/adl/compiler/messages.ts | 6 - packages/adl/compiler/util.ts | 28 ++++ packages/adl/config/config-loader.ts | 132 +++++++----------- packages/adl/config/config-validator.ts | 4 - packages/adl/config/types.ts | 7 - packages/adl/test/config/config.ts | 58 +++++++- .../test/config/scenarios/json/.adlrc.json | 5 + .../scenarios/package-json/package.json | 5 + .../scenarios/{yaml => yaml-json}/.adlrc.json | 0 .../config/scenarios/yaml-json/.adlrc.yaml | 4 + .../test/config/scenarios/yaml/.adlrc.yaml | 4 + 11 files changed, 146 insertions(+), 107 deletions(-) rename packages/adl/test/config/scenarios/{yaml => yaml-json}/.adlrc.json (100%) diff --git a/packages/adl/compiler/messages.ts b/packages/adl/compiler/messages.ts index 133bd4cb3..5b0f23b4f 100644 --- a/packages/adl/compiler/messages.ts +++ b/packages/adl/compiler/messages.ts @@ -65,12 +65,6 @@ export const Message = { text: `File {0} is not found.`, severity: "error", } as const, - - InvalidConfigFormat: { - code: 1110, - text: `File {0} does not have a known configuration format.`, - severity: "error", - } as const, }; // Static assert: this won't compile if one of the entries above is invalid. diff --git a/packages/adl/compiler/util.ts b/packages/adl/compiler/util.ts index b620078a4..5fee3e894 100644 --- a/packages/adl/compiler/util.ts +++ b/packages/adl/compiler/util.ts @@ -42,6 +42,34 @@ export function reportDuplicateSymbols(symbols: SymbolTable) { } } +export function deepFreeze(value: T): T { + if (Array.isArray(value)) { + value.map(deepFreeze); + } else if (typeof value === "object") { + for (const prop in value) { + deepFreeze(value[prop]); + } + } + + return Object.freeze(value); +} + +export function deepClone(value: T): T { + if (Array.isArray(value)) { + return value.map(deepClone) as any; + } + + if (typeof value === "object") { + const obj: any = {}; + for (const prop in value) { + obj[prop] = deepClone(value[prop]); + } + return obj; + } + + return value; +} + export const NodeHost: CompilerHost = { readFile: (path: string) => readFile(path, "utf-8"), readDir: (path: string) => readdir(path, { withFileTypes: true }), diff --git a/packages/adl/config/config-loader.ts b/packages/adl/config/config-loader.ts index ff86f67c6..85965ce1d 100644 --- a/packages/adl/config/config-loader.ts +++ b/packages/adl/config/config-loader.ts @@ -1,19 +1,19 @@ -import { access, readFile } from "fs/promises"; +import { readFile } from "fs/promises"; import { basename, extname, join } from "path"; -import { createSourceFile, Message, NoTarget, throwDiagnostic } from "../compiler/diagnostics.js"; +import { createSourceFile, throwDiagnostic } from "../compiler/diagnostics.js"; +import { deepClone, deepFreeze } from "../compiler/util.js"; import { ConfigValidator } from "./config-validator.js"; -import { ADLConfig, ADLRawConfig, ConfigFile } from "./types.js"; +import { ADLConfig } from "./types.js"; const configFilenames = [".adlrc.yaml", ".adlrc.yml", ".adlrc.json", "package.json"]; - -const defaultConfig: ADLConfig = { +const defaultConfig: ADLConfig = deepFreeze({ plugins: [], emitters: {}, lint: { extends: [], rules: {}, }, -}; +}); /** * Load ADL Configuration if present. @@ -22,11 +22,16 @@ const defaultConfig: ADLConfig = { export async function loadADLConfigInDir(directoryPath: string): Promise { for (const filename of configFilenames) { const filePath = join(directoryPath, filename); - if (await fileExists(filePath)) { - return loadADLConfigFile(filePath); + try { + return await loadADLConfigFile(filePath); + } catch (e) { + if (e.code === "ENOENT") { + continue; + } + throw e; } } - return defaultConfig; + return deepClone(defaultConfig); } /** @@ -45,99 +50,58 @@ export async function loadADLConfigFile(filePath: string): Promise { return loadYAMLConfigFile(filePath); default: - throwDiagnostic(Message.InvalidConfigFormat, NoTarget, [filePath]); + // This is not a diagnostic because the compiler only tries the + // well-known config file names. + throw new RangeError("Config file must have .yaml, .yml, or .json extension."); } } export async function loadPackageJSONConfigFile(filePath: string): Promise { - const rawConfig = await loadJSON(filePath); - if (rawConfig.data.adl) { - return normalizeConfig({ file: rawConfig.file, data: rawConfig.data.adl }); - } else { - return defaultConfig; - } + return await loadConfigFile(filePath, (content) => JSON.parse(content).adl ?? {}); } export async function loadJSONConfigFile(filePath: string): Promise { - const rawConfig = await loadJSON(filePath); - return normalizeConfig(rawConfig); + return await loadConfigFile(filePath, JSON.parse); } -/** - * Loads a YAML configuration from a file. - * @param filePath Path to the file. - */ export async function loadYAMLConfigFile(filePath: string): Promise { - const rawConfig = await loadYaml(filePath); - return normalizeConfig(rawConfig); -} - -/** - * Load YAML and throw @see DiagnosticError if there is an issue. - * @param filePath Yaml file path. - * @returns Parsed object. - */ -async function loadYaml(filePath: string): Promise> { // Lazy load. const jsyaml = await import("js-yaml"); - - const content = (await readFile(filePath)).toString(); - const file = createSourceFile(content, filePath); - try { - return { - file, - data: jsyaml.load(content) as ADLRawConfig, - }; - } catch (e) { - throwDiagnostic(e.message, { file, pos: 0, end: 0 }); - } -} - -/** - * Load JSON and throw @see DiagnosticError if there is an issue. - * @param filePath JSON file path. - * @returns Parsed object. - */ -async function loadJSON(filePath: string): Promise> { - const content = (await readFile(filePath)).toString(); - const file = createSourceFile(content, filePath); - - try { - return { - file, - data: JSON.parse(content), - }; - } catch (e) { - throwDiagnostic(e.message, { file, pos: 0, end: 0 }); - } + return await loadConfigFile(filePath, jsyaml.load); } const configValidator = new ConfigValidator(); -export function normalizeConfig(config: ConfigFile): ADLConfig { - configValidator.validateConfig(config.data, config.file); - return { - filename: config.file.path, - ...defaultConfig, - ...config.data, - lint: { - ...defaultConfig.lint, - ...(config.data.lint ?? {}), - }, - }; + +async function loadConfigFile( + filePath: string, + loadData: (content: string) => any +): Promise { + const content = await readFile(filePath, "utf-8"); + const file = createSourceFile(content, filePath); + + let config: any; + try { + config = loadData(content); + } catch (e) { + throwDiagnostic(e.message, { file, pos: 0, end: 0 }); + } + + configValidator.validateConfig(config, file); + mergeDefaults(config, defaultConfig); + config.filename = filePath; + return config; } /** - * Validate the given config is valid. + * Recursively add properties from defaults that are not present in target. */ -export function validateConfig(config: ADLRawConfig) { - return {} as any; -} - -async function fileExists(path: string) { - try { - await access(path); - return true; - } catch { - return false; +function mergeDefaults(target: any, defaults: any) { + for (const prop in defaults) { + const value = target[prop]; + if (value === undefined) { + target[prop] = deepClone(defaults[prop]); + } else if (typeof value === "object" && typeof defaults[prop] === "object") { + mergeDefaults(value, defaults[prop]); + } } } diff --git a/packages/adl/config/config-validator.ts b/packages/adl/config/config-validator.ts index faaaed81c..42bf81656 100644 --- a/packages/adl/config/config-validator.ts +++ b/packages/adl/config/config-validator.ts @@ -16,10 +16,6 @@ export class ConfigValidator { * @returns */ public validateConfig(config: ADLRawConfig, file?: SourceFile) { - this.validateSchema(config, file); - } - - private validateSchema(config: ADLRawConfig, file?: SourceFile) { const validate = this.ajv.compile(ADLConfigJsonSchema); const valid = validate(config); diff --git a/packages/adl/config/types.ts b/packages/adl/config/types.ts index e078130fb..de738b41c 100644 --- a/packages/adl/config/types.ts +++ b/packages/adl/config/types.ts @@ -1,5 +1,3 @@ -import { SourceFile } from "../compiler"; - /** * Represent the normalized user configuration. */ @@ -29,8 +27,3 @@ export interface ADLRawConfig { lint?: Partial; emitters?: Record; } - -export interface ConfigFile { - file: SourceFile; - data: T; -} diff --git a/packages/adl/test/config/config.ts b/packages/adl/test/config/config.ts index 07b1f4a55..d51a7f632 100644 --- a/packages/adl/test/config/config.ts +++ b/packages/adl/test/config/config.ts @@ -7,7 +7,7 @@ import { loadADLConfigInDir } from "../../config/index.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); -describe("adl: Config file loading", () => { +describe("adl: config file loading", () => { describe("file discovery", async () => { const scenarioRoot = resolve(__dirname, "../../../test/config/scenarios"); const loadTestConfig = async (folderName: string) => { @@ -25,24 +25,26 @@ describe("adl: Config file loading", () => { }, lint: { extends: [], - rules: {}, + rules: { + "some-rule": "on", + }, }, }); }; - it("Loads yaml config file", async () => { + it("loads yaml config file", async () => { await assertLoadFromFolder("yaml"); }); - it("Loads json config file", async () => { + it("loads json config file", async () => { await assertLoadFromFolder("json"); }); - it("Loads from adl section in package.json config file", async () => { + it("loads from adl section in package.json config file", async () => { await assertLoadFromFolder("package-json"); }); - it("Loads empty config if it can't find any config files", async () => { + it("loads empty config if it can't find any config files", async () => { const config = await loadTestConfig("empty"); deepStrictEqual(config, { plugins: [], @@ -58,6 +60,50 @@ describe("adl: Config file loading", () => { // Should load .adlrc.yaml and MOT load .adlrc.json here await assertLoadFromFolder("yaml-json"); }); + + it("deep clones defaults when not found", async () => { + // load and mutate + let config = await loadTestConfig("empty"); + config.plugins.push("x"); + config.emitters["x"] = true; + config.lint.extends.push("x"); + config.lint.rules["x"] = "off"; + + // reload and make sure mutation is not observed + config = await loadTestConfig("empty"); + deepStrictEqual(config, { + plugins: [], + emitters: {}, + lint: { + extends: [], + rules: {}, + }, + }); + }); + + it("deep clones defaults when found", async () => { + // load and mutate + let config = await loadTestConfig("yaml"); + config.plugins.push("x"); + config.emitters["x"] = true; + config.lint.extends.push("x"); + config.lint.rules["x"] = "off"; + + // reload and make sure mutation is not observed + config = await loadTestConfig("yaml"); + deepStrictEqual(config, { + plugins: ["foo"], + emitters: { + "foo:openapi": true, + }, + lint: { + extends: [], + rules: { + "some-rule": "on", + }, + }, + }); + }); }); describe("validation", () => { diff --git a/packages/adl/test/config/scenarios/json/.adlrc.json b/packages/adl/test/config/scenarios/json/.adlrc.json index 9a302c2d8..1dc207a32 100644 --- a/packages/adl/test/config/scenarios/json/.adlrc.json +++ b/packages/adl/test/config/scenarios/json/.adlrc.json @@ -2,5 +2,10 @@ "plugins": ["foo"], "emitters": { "foo:openapi": true + }, + "lint": { + "rules": { + "some-rule": "on" + } } } diff --git a/packages/adl/test/config/scenarios/package-json/package.json b/packages/adl/test/config/scenarios/package-json/package.json index cc2f4f645..a569bcab5 100644 --- a/packages/adl/test/config/scenarios/package-json/package.json +++ b/packages/adl/test/config/scenarios/package-json/package.json @@ -5,6 +5,11 @@ ], "emitters": { "foo:openapi": true + }, + "lint": { + "rules": { + "some-rule": "on" + } } } } diff --git a/packages/adl/test/config/scenarios/yaml/.adlrc.json b/packages/adl/test/config/scenarios/yaml-json/.adlrc.json similarity index 100% rename from packages/adl/test/config/scenarios/yaml/.adlrc.json rename to packages/adl/test/config/scenarios/yaml-json/.adlrc.json diff --git a/packages/adl/test/config/scenarios/yaml-json/.adlrc.yaml b/packages/adl/test/config/scenarios/yaml-json/.adlrc.yaml index 90baaf1ea..6a0d62b9a 100644 --- a/packages/adl/test/config/scenarios/yaml-json/.adlrc.yaml +++ b/packages/adl/test/config/scenarios/yaml-json/.adlrc.yaml @@ -4,3 +4,7 @@ plugins: # This has comments emitters: foo:openapi: true + +lint: + rules: + "some-rule": "on" diff --git a/packages/adl/test/config/scenarios/yaml/.adlrc.yaml b/packages/adl/test/config/scenarios/yaml/.adlrc.yaml index 90baaf1ea..cf462ad4d 100644 --- a/packages/adl/test/config/scenarios/yaml/.adlrc.yaml +++ b/packages/adl/test/config/scenarios/yaml/.adlrc.yaml @@ -4,3 +4,7 @@ plugins: # This has comments emitters: foo:openapi: true + +lint: + rules: + some-rule: on From c62c1036f94a48a55937b2c31a9be51b52c059b7 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 3 Jun 2021 08:46:10 -0700 Subject: [PATCH 40/47] Add semantic error recovery (#574) We now continue to semantic analysis when there are parse errors, and we do not abort the compilation when a semantic error is encountered, but instead recover and look for more errors. DiagnosticError is no longer a thing as we never throw diagnostics as exceptions anymore. throwDiagnostic is now Program.reportDiagnostic. --- packages/adl-rest/src/rest.ts | 31 +++- packages/adl/compiler/binder.ts | 3 +- packages/adl/compiler/checker.ts | 118 ++++++++---- packages/adl/compiler/cli.ts | 52 ++---- packages/adl/compiler/diagnostics.ts | 40 +--- packages/adl/compiler/parser.ts | 15 +- packages/adl/compiler/program.ts | 97 ++++++++-- packages/adl/compiler/scanner.ts | 10 +- packages/adl/compiler/types.ts | 4 + packages/adl/compiler/util.ts | 32 +--- packages/adl/config/config-loader.ts | 33 +++- packages/adl/config/config-validator.ts | 19 +- packages/adl/config/types.ts | 7 + packages/adl/lib/decorators.ts | 173 ++++++++++-------- .../adl/test/checker/check-parse-errors.ts | 27 +++ packages/adl/test/checker/duplicate-ids.ts | 35 ++-- packages/adl/test/config/config.ts | 49 +++-- packages/adl/test/test-host.ts | 51 ++++-- packages/adl/test/test-parser.ts | 10 +- packages/adl/test/test-scanner.ts | 14 +- 20 files changed, 480 insertions(+), 340 deletions(-) create mode 100644 packages/adl/test/checker/check-parse-errors.ts diff --git a/packages/adl-rest/src/rest.ts b/packages/adl-rest/src/rest.ts index 30e91896c..ff07dc81f 100644 --- a/packages/adl-rest/src/rest.ts +++ b/packages/adl-rest/src/rest.ts @@ -1,4 +1,4 @@ -import { NamespaceType, Program, throwDiagnostic, Type } from "@azure-tools/adl"; +import { NamespaceType, Program, Type } from "@azure-tools/adl"; const basePathsKey = Symbol(); export function resource(program: Program, entity: Type, basePath = "") { @@ -77,10 +77,10 @@ function setOperationRoute(program: Program, entity: Type, verb: OperationRoute) if (!program.stateMap(operationRoutesKey).has(entity)) { program.stateMap(operationRoutesKey).set(entity, verb); } else { - throwDiagnostic(`HTTP verb already applied to ${entity.name}`, entity); + program.reportDiagnostic(`HTTP verb already applied to ${entity.name}`, entity); } } else { - throwDiagnostic(`Cannot use @${verb} on a ${entity.kind}`, entity); + program.reportDiagnostic(`Cannot use @${verb} on a ${entity.kind}`, entity); } } @@ -145,7 +145,10 @@ function getServiceDetails(program: Program) { export function _setServiceNamespace(program: Program, namespace: NamespaceType): void { const serviceDetails = getServiceDetails(program); if (serviceDetails.namespace && serviceDetails.namespace !== namespace) { - throwDiagnostic("Cannot set service namespace more than once in an ADL project.", namespace); + program.reportDiagnostic( + "Cannot set service namespace more than once in an ADL project.", + namespace + ); } serviceDetails.namespace = namespace; @@ -159,11 +162,15 @@ export function _checkIfServiceNamespace(program: Program, namespace: NamespaceT export function serviceTitle(program: Program, entity: Type, title: string) { const serviceDetails = getServiceDetails(program); if (serviceDetails.title) { - throwDiagnostic("Service title can only be set once per ADL document.", entity); + program.reportDiagnostic("Service title can only be set once per ADL document.", entity); } if (entity.kind !== "Namespace") { - throwDiagnostic("The @serviceTitle decorator can only be applied to namespaces.", entity); + program.reportDiagnostic( + "The @serviceTitle decorator can only be applied to namespaces.", + entity + ); + return; } _setServiceNamespace(program, entity); @@ -179,11 +186,15 @@ export function serviceVersion(program: Program, entity: Type, version: string) const serviceDetails = getServiceDetails(program); // TODO: This will need to change once we support multiple service versions if (serviceDetails.version) { - throwDiagnostic("Service version can only be set once per ADL document.", entity); + program.reportDiagnostic("Service version can only be set once per ADL document.", entity); } if (entity.kind !== "Namespace") { - throwDiagnostic("The @serviceVersion decorator can only be applied to namespaces.", entity); + program.reportDiagnostic( + "The @serviceVersion decorator can only be applied to namespaces.", + entity + ); + return; } _setServiceNamespace(program, entity); @@ -207,7 +218,7 @@ const producesTypesKey = Symbol(); export function produces(program: Program, entity: Type, ...contentTypes: string[]) { if (entity.kind !== "Namespace") { - throwDiagnostic("The @produces decorator can only be applied to namespaces.", entity); + program.reportDiagnostic("The @produces decorator can only be applied to namespaces.", entity); } const values = getProduces(program, entity); @@ -222,7 +233,7 @@ const consumesTypesKey = Symbol(); export function consumes(program: Program, entity: Type, ...contentTypes: string[]) { if (entity.kind !== "Namespace") { - throwDiagnostic("The @consumes decorator can only be applied to namespaces.", entity); + program.reportDiagnostic("The @consumes decorator can only be applied to namespaces.", entity); } const values = getConsumes(program, entity); diff --git a/packages/adl/compiler/binder.ts b/packages/adl/compiler/binder.ts index e9a339365..17188cf80 100644 --- a/packages/adl/compiler/binder.ts +++ b/packages/adl/compiler/binder.ts @@ -17,7 +17,6 @@ import { TemplateParameterDeclarationNode, UsingStatementNode, } from "./types.js"; -import { reportDuplicateSymbols } from "./util.js"; const SymbolTable = class extends Map implements SymbolTable { duplicates = new Set(); @@ -56,7 +55,7 @@ export function createSymbolTable(): SymbolTable { return new SymbolTable(); } -export function createBinder(): Binder { +export function createBinder(reportDuplicateSymbols: (symbolTable: SymbolTable) => void): Binder { let currentFile: ADLScriptNode; let parentNode: Node; let globalNamespace: NamespaceStatementNode; diff --git a/packages/adl/compiler/checker.ts b/packages/adl/compiler/checker.ts index f979ed9c2..ee03c2343 100644 --- a/packages/adl/compiler/checker.ts +++ b/packages/adl/compiler/checker.ts @@ -1,4 +1,5 @@ -import { compilerAssert, throwDiagnostic } from "./diagnostics.js"; +import { compilerAssert } from "./diagnostics.js"; +import { hasParseError } from "./parser.js"; import { Program } from "./program.js"; import { ADLScriptNode, @@ -11,9 +12,9 @@ import { EnumMemberType, EnumStatementNode, EnumType, + ErrorType, IdentifierNode, IntersectionExpressionNode, - IntrinsicType, LiteralNode, LiteralType, ModelExpressionNode, @@ -44,7 +45,6 @@ import { UnionExpressionNode, UnionType, } from "./types.js"; -import { reportDuplicateSymbols } from "./util.js"; /** * A map keyed by a set of objects. @@ -97,7 +97,7 @@ export function createChecker(program: Program) { let instantiatingTemplate: Node | undefined; let currentSymbolId = 0; const symbolLinks = new Map(); - const errorType: IntrinsicType = { kind: "Intrinsic", name: "ErrorType" }; + const errorType: ErrorType = { kind: "Intrinsic", name: "ErrorType" }; // This variable holds on to the model type that is currently // being instantiated in checkModelStatement so that it is @@ -108,9 +108,17 @@ export function createChecker(program: Program) { for (const using of file.usings) { const parentNs = using.parent! as NamespaceStatementNode | ADLScriptNode; const sym = resolveTypeReference(using.name); - if (sym.kind === "decorator") throwDiagnostic("Can't use a decorator", using); + if (!sym) { + continue; + } + if (sym.kind === "decorator") { + program.reportDiagnostic("Can't use a decorator", using); + continue; + } + if (sym.node.kind !== SyntaxKind.NamespaceStatement) { - throwDiagnostic("Using must refer to a namespace", using); + program.reportDiagnostic("Using must refer to a namespace", using); + continue; } for (const [name, binding] of sym.node.exports!) { @@ -231,12 +239,17 @@ export function createChecker(program: Program) { function checkTypeReference(node: TypeReferenceNode): Type { const sym = resolveTypeReference(node); + if (!sym) { + return errorType; + } + if (sym.kind === "decorator") { - throwDiagnostic("Can't put a decorator in a type", node); + program.reportDiagnostic("Can't put a decorator in a type", node); + return errorType; } const symbolLinks = getSymbolLinks(sym); - const args = node.arguments.map(getTypeForNode); + let args = node.arguments.map(getTypeForNode); if ( sym.node.kind === SyntaxKind.ModelStatement || @@ -245,7 +258,10 @@ export function createChecker(program: Program) { // model statement, possibly templated if (sym.node.templateParameters.length === 0) { if (args.length > 0) { - throwDiagnostic("Can't pass template arguments to model that is not templated", node); + program.reportDiagnostic( + "Can't pass template arguments to model that is not templated", + node + ); } if (symbolLinks.declaredType) { @@ -267,21 +283,21 @@ export function createChecker(program: Program) { : checkAlias(sym.node); } - if (sym.node.templateParameters!.length > node.arguments.length) { - throwDiagnostic("Too few template arguments provided.", node); + const templateParameters = sym.node.templateParameters; + if (args.length < templateParameters.length) { + program.reportDiagnostic("Too few template arguments provided.", node); + args = [...args, ...new Array(templateParameters.length - args.length).fill(errorType)]; + } else if (args.length > templateParameters.length) { + program.reportDiagnostic("Too many template arguments provided.", node); + args = args.slice(0, templateParameters.length); } - - if (sym.node.templateParameters!.length < node.arguments.length) { - throwDiagnostic("Too many template arguments provided.", node); - } - return instantiateTemplate(sym.node, args); } } // some other kind of reference if (args.length > 0) { - throwDiagnostic("Can't pass template arguments to non-templated type", node); + program.reportDiagnostic("Can't pass template arguments to non-templated type", node); } if (sym.node.kind === SyntaxKind.TemplateParameterDeclaration) { @@ -363,7 +379,8 @@ export function createChecker(program: Program) { function checkIntersectionExpression(node: IntersectionExpressionNode) { const optionTypes = node.options.map(getTypeForNode); if (!allModelTypes(optionTypes)) { - throwDiagnostic("Cannot intersect non-model types (including union types).", node); + program.reportDiagnostic("Cannot intersect non-model types (including union types).", node); + return errorType; } const properties = new Map(); @@ -371,10 +388,11 @@ export function createChecker(program: Program) { const allProps = walkPropertiesInherited(option); for (const prop of allProps) { if (properties.has(prop.name)) { - throwDiagnostic( + program.reportDiagnostic( `Intersection contains duplicate property definitions for ${prop.name}`, node ); + continue; } const newPropType = createType({ @@ -510,6 +528,12 @@ export function createChecker(program: Program) { } function resolveIdentifier(node: IdentifierNode) { + if (hasParseError(node)) { + // Don't report synthetic identifiers used for parser error recovery. + // The parse error is the root cause and will already have been logged. + return undefined; + } + let scope: Node | undefined = node.parent; let binding; @@ -539,29 +563,38 @@ export function createChecker(program: Program) { if (binding) return binding; } - throwDiagnostic("Unknown identifier " + node.sv, node); + program.reportDiagnostic("Unknown identifier " + node.sv, node); + return undefined; } - function resolveTypeReference(node: ReferenceExpression): DecoratorSymbol | TypeSymbol { + function resolveTypeReference( + node: ReferenceExpression + ): DecoratorSymbol | TypeSymbol | undefined { if (node.kind === SyntaxKind.TypeReference) { return resolveTypeReference(node.target); } if (node.kind === SyntaxKind.MemberExpression) { const base = resolveTypeReference(node.base); + if (!base) { + return undefined; + } if (base.kind === "type" && base.node.kind === SyntaxKind.NamespaceStatement) { const symbol = resolveIdentifierInTable(node.id, base.node.exports!); if (!symbol) { - throwDiagnostic(`Namespace doesn't have member ${node.id.sv}`, node); + program.reportDiagnostic(`Namespace doesn't have member ${node.id.sv}`, node); + return undefined; } return symbol; } else if (base.kind === "decorator") { - throwDiagnostic(`Cannot resolve '${node.id.sv}' in decorator`, node); + program.reportDiagnostic(`Cannot resolve '${node.id.sv}' in decorator`, node); + return undefined; } else { - throwDiagnostic( + program.reportDiagnostic( `Cannot resolve '${node.id.sv}' in non-namespace node ${base.node.kind}`, node ); + return undefined; } } @@ -585,12 +618,12 @@ export function createChecker(program: Program) { } function checkProgram(program: Program) { - reportDuplicateSymbols(program.globalNamespace.exports!); + program.reportDuplicateSymbols(program.globalNamespace.exports!); for (const file of program.sourceFiles) { - reportDuplicateSymbols(file.locals!); + program.reportDuplicateSymbols(file.locals!); for (const ns of file.namespaces) { - reportDuplicateSymbols(ns.locals!); - reportDuplicateSymbols(ns.exports!); + program.reportDuplicateSymbols(ns.locals!); + program.reportDuplicateSymbols(ns.exports!); initializeTypeForNamespace(ns); } @@ -688,7 +721,8 @@ export function createChecker(program: Program) { for (const newProp of newProperties) { if (properties.has(newProp.name)) { - throwDiagnostic(`Model already has a property named ${newProp.name}`, node); + program.reportDiagnostic(`Model already has a property named ${newProp.name}`, node); + continue; } properties.set(newProp.name, newProp); @@ -700,15 +734,20 @@ export function createChecker(program: Program) { } function checkClassHeritage(heritage: ReferenceExpression[]): ModelType[] { - return heritage.map((heritageRef) => { + let baseModels = []; + for (let heritageRef of heritage) { const heritageType = getTypeForNode(heritageRef); - - if (heritageType.kind !== "Model") { - throwDiagnostic("Models must extend other models.", heritageRef); + if (isErrorType(heritageType)) { + compilerAssert(program.hasError(), "Should already have reported an error.", heritageRef); + continue; } - - return heritageType; - }); + if (heritageType.kind !== "Model") { + program.reportDiagnostic("Models must extend other models.", heritageRef); + continue; + } + baseModels.push(heritageType); + } + return baseModels; } function checkSpreadProperty(targetNode: ReferenceExpression): ModelTypeProperty[] { @@ -717,7 +756,8 @@ export function createChecker(program: Program) { if (targetType.kind != "TemplateParameter") { if (targetType.kind !== "Model") { - throwDiagnostic("Cannot spread properties of non-model type.", targetNode); + program.reportDiagnostic("Cannot spread properties of non-model type.", targetNode); + return props; } // copy each property @@ -852,3 +892,7 @@ export function createChecker(program: Program) { return type; } } + +function isErrorType(type: Type): type is ErrorType { + return type.kind === "Intrinsic" && type.name === "ErrorType"; +} diff --git a/packages/adl/compiler/cli.ts b/packages/adl/compiler/cli.ts index ad493ef3b..c3cd1d11b 100644 --- a/packages/adl/compiler/cli.ts +++ b/packages/adl/compiler/cli.ts @@ -8,7 +8,7 @@ import yargs from "yargs"; import { CompilerOptions } from "../compiler/options.js"; import { compile } from "../compiler/program.js"; import { loadADLConfigInDir } from "../config/index.js"; -import { compilerAssert, DiagnosticError, dumpError, logDiagnostics } from "./diagnostics.js"; +import { compilerAssert, dumpError, logDiagnostics } from "./diagnostics.js"; import { formatADLFiles } from "./formatter.js"; import { adlVersion, NodeHost } from "./util.js"; @@ -107,22 +107,15 @@ const args = yargs(process.argv.slice(2)) .demandCommand(1, "You must use one of the supported commands.").argv; async function compileInput(compilerOptions: CompilerOptions, printSuccess = true) { - try { - await compile(args.path!, NodeHost, compilerOptions); - if (printSuccess) { - console.log( - `Compilation completed successfully, output files are in ${compilerOptions.outputPath}.` - ); - } - } catch (err) { - if (err instanceof DiagnosticError) { - logDiagnostics(err.diagnostics, console.error); - if (args.debug) { - console.error(`Stack trace:\n\n${err.stack}`); - } - process.exit(1); - } - throw err; // let non-diagnostic errors go to top-level bug handler. + const program = await compile(args.path!, NodeHost, compilerOptions); + logDiagnostics(program.diagnostics, console.error); + if (program.hasError()) { + process.exit(1); + } + if (printSuccess) { + console.log( + `Compilation completed successfully, output files are in ${compilerOptions.outputPath}.` + ); } } @@ -266,22 +259,15 @@ async function printInfo() { const cwd = process.cwd(); console.log(`Module: ${url.fileURLToPath(import.meta.url)}`); - try { - const config = await loadADLConfigInDir(cwd); - const jsyaml = await import("js-yaml"); - console.log(`User Config: ${config.filename ?? "No config file found"}`); - console.log("-----------"); - console.log(jsyaml.dump(config)); - console.log("-----------"); - } catch (err) { - if (err instanceof DiagnosticError) { - logDiagnostics(err.diagnostics, console.error); - if (args.debug) { - console.error(`Stack trace:\n\n${err.stack}`); - } - process.exit(1); - } - throw err; // let non-diagnostic errors go to top-level bug handler. + const config = await loadADLConfigInDir(cwd); + const jsyaml = await import("js-yaml"); + console.log(`User Config: ${config.filename ?? "No config file found"}`); + console.log("-----------"); + console.log(jsyaml.dump(config)); + console.log("-----------"); + logDiagnostics(config.diagnostics, console.error); + if (config.diagnostics.some((d) => d.severity === "error")) { + process.exit(1); } } diff --git a/packages/adl/compiler/diagnostics.ts b/packages/adl/compiler/diagnostics.ts index 03adaebe4..a26da296c 100644 --- a/packages/adl/compiler/diagnostics.ts +++ b/packages/adl/compiler/diagnostics.ts @@ -5,19 +5,6 @@ import { Diagnostic, Node, SourceFile, SourceLocation, Sym, SyntaxKind, Type } f export { Message } from "./messages.js"; -/** - * Represents an error in the code input that is fatal and bails the compilation. - * - * This isn't meant to be kept long term, but we currently do this on all errors. - */ -export class DiagnosticError extends Error { - constructor(public readonly diagnostics: readonly Diagnostic[]) { - super("Code diagnostics. See diagnostics array."); - // Tests don't have our catch-all handler so log the diagnostic now. - logVerboseTestOutput((log) => logDiagnostics(diagnostics, log)); - } -} - /** * Represents a failure with multiple errors. */ @@ -35,22 +22,7 @@ export class AggregateError extends Error { export const NoTarget = Symbol("NoTarget"); export type DiagnosticTarget = Node | Type | Sym | SourceLocation; export type WriteLine = (text?: string) => void; - -export type ErrorHandler = ( - message: Message | string, - target: DiagnosticTarget, - args?: (string | number)[] -) => void; - -export const throwOnError: ErrorHandler = throwDiagnostic; - -export function throwDiagnostic( - message: Message | string, - target: DiagnosticTarget | typeof NoTarget, - args?: (string | number)[] -): never { - throw new DiagnosticError([createDiagnostic(message, target, args)]); -} +export type DiagnosticHandler = (diagnostic: Diagnostic) => void; export function createDiagnostic( message: Message | string, @@ -82,7 +54,10 @@ export function createDiagnostic( }; if (locationError || formatError) { - throw new AggregateError(new DiagnosticError([diagnostic]), locationError, formatError); + const diagnosticError = new Error( + "Error(s) occurred trying to report diagnostic: " + diagnostic.message + ); + throw new AggregateError(diagnosticError, locationError, formatError); } return diagnostic; @@ -206,10 +181,7 @@ export function logVerboseTestOutput(messageOrCallback: string | ((log: WriteLin } export function dumpError(error: Error, writeLine: WriteLine) { - if (error instanceof DiagnosticError) { - logDiagnostics(error.diagnostics, writeLine); - writeLine(error.stack); - } else if (error instanceof AggregateError) { + if (error instanceof AggregateError) { for (const inner of error.errors) { dumpError(inner, writeLine); } diff --git a/packages/adl/compiler/parser.ts b/packages/adl/compiler/parser.ts index 7419a28d6..8803ad0eb 100644 --- a/packages/adl/compiler/parser.ts +++ b/packages/adl/compiler/parser.ts @@ -1,5 +1,5 @@ import { createSymbolTable } from "./binder.js"; -import { compilerAssert, createDiagnostic, DiagnosticTarget, Message } from "./diagnostics.js"; +import { compilerAssert, createDiagnostic } from "./diagnostics.js"; import { createScanner, isComment, @@ -1028,19 +1028,14 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): AD return; } realPositionOfLastError = realPos; - - reportDiagnostic(message, location); + const diagnostic = createDiagnostic(message, location); + reportDiagnostic(diagnostic); } - function reportDiagnostic( - message: Message | string, - target: DiagnosticTarget, - args?: (string | number)[] - ) { - if (typeof message === "string" || message.severity === "error") { + function reportDiagnostic(diagnostic: Diagnostic) { + if (diagnostic.severity === "error") { parseErrorInNextFinishedNode = true; } - const diagnostic = createDiagnostic(message, target, args); parseDiagnostics.push(diagnostic); } diff --git a/packages/adl/compiler/program.ts b/packages/adl/compiler/program.ts index 839d2075c..179a8ee8e 100644 --- a/packages/adl/compiler/program.ts +++ b/packages/adl/compiler/program.ts @@ -2,7 +2,7 @@ import { dirname, extname, isAbsolute, join, resolve } from "path"; import resolveModule from "resolve"; import { createBinder, createSymbolTable } from "./binder.js"; import { createChecker } from "./checker.js"; -import { createSourceFile, DiagnosticError, NoTarget, throwDiagnostic } from "./diagnostics.js"; +import { createDiagnostic, createSourceFile, DiagnosticTarget, NoTarget } from "./diagnostics.js"; import { Message } from "./messages.js"; import { CompilerOptions } from "./options.js"; import { parse } from "./parser.js"; @@ -11,11 +11,14 @@ import { CompilerHost, DecoratorExpressionNode, DecoratorSymbol, + Diagnostic, IdentifierNode, LiteralType, ModelStatementNode, ModelType, NamespaceStatementNode, + Sym, + SymbolTable, SyntaxKind, Type, } from "./types.js"; @@ -27,6 +30,7 @@ export interface Program { literalTypes: Map; host: CompilerHost; checker?: ReturnType; + readonly diagnostics: readonly Diagnostic[]; evalAdlScript(adlScript: string, filePath?: string): void; onBuild(cb: (program: Program) => void): Promise | void; getOption(key: string): string | undefined; @@ -35,6 +39,15 @@ export interface Program { executeDecorator(node: DecoratorExpressionNode, program: Program, type: Type): void; stateSet(key: Symbol): Set; stateMap(key: Symbol): Map; + hasError(): boolean; + reportDiagnostic( + message: Message | string, + target: DiagnosticTarget | typeof NoTarget, + args?: (string | number)[] + ): void; + reportDiagnostic(diagnostic: Diagnostic): void; + reportDiagnostics(diagnostics: Diagnostic[]): void; + reportDuplicateSymbols(symbols: SymbolTable): void; } export async function createProgram( @@ -44,14 +57,18 @@ export async function createProgram( const buildCbs: any = []; const stateMaps = new Map>(); const stateSets = new Map>(); - + const diagnostics: Diagnostic[] = []; const seenSourceFiles = new Set(); + const duplicateSymbols = new Set(); + let error = false; + const program: Program = { compilerOptions: options || {}, globalNamespace: createGlobalNamespace(), sourceFiles: [], literalTypes: new Map(), host, + diagnostics, evalAdlScript, executeModelDecorators, executeDecorators, @@ -59,13 +76,19 @@ export async function createProgram( getOption, stateMap, stateSet, + reportDiagnostic, + reportDiagnostics, + reportDuplicateSymbols, + hasError() { + return error; + }, onBuild(cb) { buildCbs.push(cb); }, }; let virtualFileCount = 0; - const binder = createBinder(); + const binder = createBinder(program.reportDuplicateSymbols); if (!options?.nostdlib) { await loadStandardLibrary(program); @@ -131,14 +154,16 @@ export async function createProgram( function executeDecorator(dec: DecoratorExpressionNode, program: Program, type: Type) { if (dec.target.kind !== SyntaxKind.Identifier) { - throwDiagnostic("Decorator must be identifier", dec); + program.reportDiagnostic("Decorator must be identifier", dec); + return; } const decName = dec.target.sv; const args = dec.arguments.map((a) => toJSON(checker.getTypeForNode(a))); const decBinding = program.globalNamespace.locals!.get(decName) as DecoratorSymbol; if (!decBinding) { - throwDiagnostic(`Can't find decorator ${decName}`, dec); + program.reportDiagnostic(`Can't find decorator ${decName}`, dec); + return; } const decFn = decBinding.value; decFn(program, type, ...args); @@ -219,11 +244,7 @@ export async function createProgram( const unparsedFile = createSourceFile(adlScript, filePath); const sourceFile = parse(unparsedFile); - // We don't attempt to evaluate yet when there are parse errors. - if (sourceFile.parseDiagnostics.length > 0) { - throw new DiagnosticError(sourceFile.parseDiagnostics); - } - + program.reportDiagnostics(sourceFile.parseDiagnostics); program.sourceFiles.push(sourceFile); binder.bindSourceFile(program, sourceFile); await evalImports(sourceFile); @@ -247,7 +268,8 @@ export async function createProgram( target = await resolveModuleSpecifier(path, basedir); } catch (e) { if (e.code === "MODULE_NOT_FOUND") { - throwDiagnostic(`Couldn't find library "${path}"`, stmt); + program.reportDiagnostic(`Couldn't find library "${path}"`, stmt); + continue; } else { throw e; } @@ -264,7 +286,7 @@ export async function createProgram( } else if (ext === ".adl") { await loadAdlFile(target); } else { - throwDiagnostic( + program.reportDiagnostic( "Import paths must reference either a directory, a .adl file, or .js file", stmt ); @@ -351,6 +373,9 @@ export async function createProgram( const mainPath = resolve(host.getCwd(), options.mainFile); const mainStat = await getMainPathStats(mainPath); + if (!mainStat) { + return; + } if (mainStat.isDirectory()) { await loadDirectory(mainPath); } else { @@ -363,7 +388,8 @@ export async function createProgram( return await host.stat(mainPath); } catch (e) { if (e.code === "ENOENT") { - throwDiagnostic(Message.FileNotFound, NoTarget, [mainPath]); + program.reportDiagnostic(Message.FileNotFound, NoTarget, [mainPath]); + return undefined; } throw e; } @@ -392,8 +418,49 @@ export async function createProgram( return s; } + + function reportDiagnostic(diagnostic: Diagnostic): void; + + function reportDiagnostic( + message: Message | string, + target: DiagnosticTarget | typeof NoTarget, + args?: (string | number)[] + ): void; + + function reportDiagnostic( + diagnostic: Message | string | Diagnostic, + target?: DiagnosticTarget | typeof NoTarget, + args?: (string | number)[] + ): void { + if (typeof diagnostic === "string" || "text" in diagnostic) { + diagnostic = createDiagnostic(diagnostic, target!, args); + } + if (diagnostic.severity === "error") { + error = true; + } + diagnostics.push(diagnostic); + } + + function reportDiagnostics(newDiagnostics: Diagnostic[]) { + for (const diagnostic of newDiagnostics) { + reportDiagnostic(diagnostic); + } + } + + function reportDuplicateSymbols(symbols: SymbolTable) { + for (const symbol of symbols.duplicates) { + if (!duplicateSymbols.has(symbol)) { + duplicateSymbols.add(symbol); + reportDiagnostic("Duplicate name: " + symbol.name, symbol); + } + } + } } -export async function compile(rootDir: string, host: CompilerHost, options?: CompilerOptions) { - const program = await createProgram(host, { mainFile: rootDir, ...options }); +export async function compile( + rootDir: string, + host: CompilerHost, + options?: CompilerOptions +): Promise { + return await createProgram(host, { mainFile: rootDir, ...options }); } diff --git a/packages/adl/compiler/scanner.ts b/packages/adl/compiler/scanner.ts index c80118ce5..d103edcd7 100644 --- a/packages/adl/compiler/scanner.ts +++ b/packages/adl/compiler/scanner.ts @@ -15,7 +15,7 @@ import { isWhiteSpaceSingleLine, utf16CodeUnits, } from "./charcode.js"; -import { createSourceFile, Message, throwOnError } from "./diagnostics.js"; +import { createDiagnostic, createSourceFile, DiagnosticHandler, Message } from "./diagnostics.js"; import { SourceFile } from "./types.js"; // All conflict markers consist of the same character repeated seven times. If it is @@ -244,7 +244,10 @@ export function isStatementKeyword(token: Token) { return token >= MinStatementKeyword && token <= MaxStatementKeyword; } -export function createScanner(source: string | SourceFile, onError = throwOnError): Scanner { +export function createScanner( + source: string | SourceFile, + diagnosticHandler: DiagnosticHandler +): Scanner { const file = typeof source === "string" ? createSourceFile(source, "") : source; const input = file.text; let position = 0; @@ -479,7 +482,8 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro } function error(msg: Message, args?: (string | number)[]) { - onError(msg, { file, pos: tokenPosition, end: position }, args); + const diagnostic = createDiagnostic(msg, { file, pos: tokenPosition, end: position }, args); + diagnosticHandler(diagnostic); } function scanWhitespace(): Token { diff --git a/packages/adl/compiler/types.ts b/packages/adl/compiler/types.ts index 6f58da30c..c4f4cf685 100644 --- a/packages/adl/compiler/types.ts +++ b/packages/adl/compiler/types.ts @@ -28,6 +28,10 @@ export interface IntrinsicType extends BaseType { name: string; } +export interface ErrorType extends IntrinsicType { + name: "ErrorType"; +} + export interface ModelType extends BaseType { kind: "Model"; name: string; diff --git a/packages/adl/compiler/util.ts b/packages/adl/compiler/util.ts index 5fee3e894..46f9e6b2d 100644 --- a/packages/adl/compiler/util.ts +++ b/packages/adl/compiler/util.ts @@ -2,8 +2,7 @@ import fs from "fs"; import { readdir, readFile, realpath, stat, writeFile } from "fs/promises"; import { join, resolve } from "path"; import { fileURLToPath, pathToFileURL, URL } from "url"; -import { createDiagnostic, DiagnosticError } from "./diagnostics.js"; -import { CompilerHost, Diagnostic, Sym, SymbolTable } from "./types.js"; +import { CompilerHost } from "./types.js"; export const adlVersion = getVersion(); @@ -13,35 +12,6 @@ function getVersion(): string { return packageJson.version; } -export function reportDuplicateSymbols(symbols: SymbolTable) { - let reported = new Set(); - let diagnostics: Diagnostic[] = []; - - for (const symbol of symbols.duplicates) { - report(symbol); - } - - if (diagnostics.length > 0) { - // TODO: We're now reporting all duplicates up to the binding of the first file - // that introduced one, but still bailing the compilation rather than - // recovering and reporting other issues including the possibility of more - // duplicates. - // - // That said, decorators are entered into the global symbol table before - // any source file is bound and therefore this will include all duplicate - // decorator implementations. - throw new DiagnosticError(diagnostics); - } - - function report(symbol: Sym) { - if (!reported.has(symbol)) { - reported.add(symbol); - const diagnostic = createDiagnostic("Duplicate name: " + symbol.name, symbol); - diagnostics.push(diagnostic); - } - } -} - export function deepFreeze(value: T): T { if (Array.isArray(value)) { value.map(deepFreeze); diff --git a/packages/adl/config/config-loader.ts b/packages/adl/config/config-loader.ts index 85965ce1d..5967b5419 100644 --- a/packages/adl/config/config-loader.ts +++ b/packages/adl/config/config-loader.ts @@ -1,6 +1,7 @@ import { readFile } from "fs/promises"; import { basename, extname, join } from "path"; -import { createSourceFile, throwDiagnostic } from "../compiler/diagnostics.js"; +import { createDiagnostic, createSourceFile } from "../compiler/diagnostics.js"; +import { Diagnostic } from "../compiler/types.js"; import { deepClone, deepFreeze } from "../compiler/util.js"; import { ConfigValidator } from "./config-validator.js"; import { ADLConfig } from "./types.js"; @@ -8,6 +9,7 @@ import { ADLConfig } from "./types.js"; const configFilenames = [".adlrc.yaml", ".adlrc.yml", ".adlrc.json", "package.json"]; const defaultConfig: ADLConfig = deepFreeze({ plugins: [], + diagnostics: [], emitters: {}, lint: { extends: [], @@ -79,17 +81,32 @@ async function loadConfigFile( const content = await readFile(filePath, "utf-8"); const file = createSourceFile(content, filePath); - let config: any; + let loadDiagnostics: Diagnostic[]; + let data: any; try { - config = loadData(content); + data = loadData(content); + loadDiagnostics = []; } catch (e) { - throwDiagnostic(e.message, { file, pos: 0, end: 0 }); + loadDiagnostics = [createDiagnostic(e.message, { file, pos: 0, end: 0 })]; } - configValidator.validateConfig(config, file); - mergeDefaults(config, defaultConfig); - config.filename = filePath; - return config; + const validationDiagnostics = configValidator.validateConfig(data); + const diagnostics = [...loadDiagnostics, ...validationDiagnostics]; + + if (diagnostics.some((d) => d.severity === "error")) { + // NOTE: Don't trust the data if there are validation errors, and use + // default config. Otherwise, we may return an object that does not + // conform to ADLConfig's typing. + data = defaultConfig; + } else { + mergeDefaults(data, defaultConfig); + } + + return { + ...data, + filename: filePath, + diagnostics, + }; } /** diff --git a/packages/adl/config/config-validator.ts b/packages/adl/config/config-validator.ts index 42bf81656..44cef12b1 100644 --- a/packages/adl/config/config-validator.ts +++ b/packages/adl/config/config-validator.ts @@ -1,5 +1,5 @@ import Ajv, { ErrorObject } from "ajv"; -import { DiagnosticError } from "../compiler/diagnostics.js"; +import { compilerAssert } from "../compiler/diagnostics.js"; import { Diagnostic, SourceFile } from "../compiler/types.js"; import { ADLConfigJsonSchema } from "./config-schema.js"; import { ADLRawConfig } from "./types.js"; @@ -13,19 +13,16 @@ export class ConfigValidator { * Validate the config is valid * @param config Configuration * @param file @optional file for errors tracing. - * @returns + * @returns Validation */ - public validateConfig(config: ADLRawConfig, file?: SourceFile) { + public validateConfig(config: ADLRawConfig, file?: SourceFile): Diagnostic[] { const validate = this.ajv.compile(ADLConfigJsonSchema); const valid = validate(config); - - if (!valid && validate.errors) { - throw new DiagnosticError( - validate.errors.map((error) => { - return ajvErrorToDiagnostic(error, file); - }) - ); - } + compilerAssert( + !valid || !validate.errors, + "There should be errors reported if the config file is not valid." + ); + return validate.errors?.map((e) => ajvErrorToDiagnostic(e, file)) ?? []; } } diff --git a/packages/adl/config/types.ts b/packages/adl/config/types.ts index de738b41c..16b5cf9fd 100644 --- a/packages/adl/config/types.ts +++ b/packages/adl/config/types.ts @@ -1,3 +1,5 @@ +import { Diagnostic } from "../compiler"; + /** * Represent the normalized user configuration. */ @@ -7,6 +9,11 @@ export interface ADLConfig { */ filename?: string; + /** + * Diagnostics reported while loading the configuration + */ + diagnostics: Diagnostic[]; + plugins: string[]; lint: ADLLintConfig; emitters: Record; diff --git a/packages/adl/lib/decorators.ts b/packages/adl/lib/decorators.ts index 7cb5ccfa8..b58f359f2 100644 --- a/packages/adl/lib/decorators.ts +++ b/packages/adl/lib/decorators.ts @@ -1,4 +1,3 @@ -import { throwDiagnostic } from "../compiler/diagnostics.js"; import { Program } from "../compiler/program.js"; import { ModelTypeProperty, NamespaceType, Type } from "../compiler/types.js"; @@ -26,7 +25,10 @@ export function intrinsic(program: Program, target: Type) { program.stateSet(intrinsicsKey).add(target); } -export function isIntrinsic(program: Program, target: Type) { +export function isIntrinsic(program: Program, target: Type | undefined) { + if (!target) { + return false; + } return program.stateSet(intrinsicsKey).has(target); } @@ -56,13 +58,14 @@ export function getIntrinsicType(program: Program, target: Type | undefined): st const numericTypesKey = Symbol(); export function numeric(program: Program, target: Type) { if (!isIntrinsic(program, target)) { - throwDiagnostic("Cannot apply @numeric decorator to non-intrinsic type.", target); + program.reportDiagnostic("Cannot apply @numeric decorator to non-intrinsic type.", target); + return; } - if (target.kind === "Model") { - program.stateSet(numericTypesKey).add(target.name); - } else { - throwDiagnostic("Cannot apply @numeric decorator to non-model type.", target); + if (target.kind !== "Model") { + program.reportDiagnostic("Cannot apply @numeric decorator to non-model type.", target); + return; } + program.stateSet(numericTypesKey).add(target.name); } export function isNumericType(program: Program, target: Type): boolean { @@ -75,16 +78,20 @@ export function isNumericType(program: Program, target: Type): boolean { const formatValuesKey = Symbol(); export function format(program: Program, target: Type, format: string) { - if (target.kind === "Model" || target.kind === "ModelProperty") { - // Is it a model type that ultimately derives from 'string'? - if (getIntrinsicType(program, target) === "string") { - program.stateMap(formatValuesKey).set(target, format); - } else { - throwDiagnostic("Cannot apply @format to a non-string type", target); - } - } else { - throwDiagnostic("Cannot apply @format to anything that isn't a Model or ModelProperty", target); + if (target.kind !== "Model" && target.kind !== "ModelProperty") { + program.reportDiagnostic( + "Cannot apply @format to anything that isn't a Model or ModelProperty", + target + ); + return; } + + if (getIntrinsicType(program, target) !== "string") { + program.reportDiagnostic("Cannot apply @format to a non-string type", target); + return; + } + + program.stateMap(formatValuesKey).set(target, format); } export function getFormat(program: Program, target: Type): string | undefined { @@ -96,19 +103,20 @@ export function getFormat(program: Program, target: Type): string | undefined { const minLengthValuesKey = Symbol(); export function minLength(program: Program, target: Type, minLength: number) { - if (target.kind === "Model" || target.kind === "ModelProperty") { - // Is it a model type that ultimately derives from 'string'? - if (getIntrinsicType(program, target) === "string") { - program.stateMap(minLengthValuesKey).set(target, minLength); - } else { - throwDiagnostic("Cannot apply @minLength to a non-string type", target); - } - } else { - throwDiagnostic( + if (target.kind !== "Model" && target.kind !== "ModelProperty") { + program.reportDiagnostic( "Cannot apply @minLength to anything that isn't a Model or ModelProperty", target ); + return; } + + if (getIntrinsicType(program, target) !== "string") { + program.reportDiagnostic("Cannot apply @minLength to a non-string type", target); + return; + } + + program.stateMap(minLengthValuesKey).set(target, minLength); } export function getMinLength(program: Program, target: Type): number | undefined { @@ -120,19 +128,19 @@ export function getMinLength(program: Program, target: Type): number | undefined const maxLengthValuesKey = Symbol(); export function maxLength(program: Program, target: Type, maxLength: number) { - if (target.kind === "Model" || target.kind === "ModelProperty") { - // Is it a model type that ultimately derives from 'string'? - if (getIntrinsicType(program, target) === "string") { - program.stateMap(maxLengthValuesKey).set(target, maxLength); - } else { - throwDiagnostic("Cannot apply @maxLength to a non-string type", target); - } - } else { - throwDiagnostic( + if (target.kind !== "Model" && target.kind !== "ModelProperty") { + program.reportDiagnostic( "Cannot apply @maxLength to anything that isn't a Model or ModelProperty", target ); + return; } + + if (getIntrinsicType(program, target) !== "string") { + program.reportDiagnostic("Cannot apply @maxLength to a non-string type", target); + return; + } + program.stateMap(maxLengthValuesKey).set(target, maxLength); } export function getMaxLength(program: Program, target: Type): number | undefined { @@ -144,19 +152,17 @@ export function getMaxLength(program: Program, target: Type): number | undefined const minValuesKey = Symbol(); export function minValue(program: Program, target: Type, minValue: number) { - if (target.kind === "Model" || target.kind === "ModelProperty") { - // Is it ultimately a numeric type? - if (isNumericType(program, target)) { - program.stateMap(minValuesKey).set(target, minValue); - } else { - throwDiagnostic("Cannot apply @minValue to a non-numeric type", target); - } - } else { - throwDiagnostic( + if (target.kind !== "Model" && target.kind !== "ModelProperty") { + program.reportDiagnostic( "Cannot apply @minValue to anything that isn't a Model or ModelProperty", target ); } + if (!isNumericType(program, target)) { + program.reportDiagnostic("Cannot apply @minValue to a non-numeric type", target); + return; + } + program.stateMap(minValuesKey).set(target, minValue); } export function getMinValue(program: Program, target: Type): number | undefined { @@ -168,19 +174,18 @@ export function getMinValue(program: Program, target: Type): number | undefined const maxValuesKey = Symbol(); export function maxValue(program: Program, target: Type, maxValue: number) { - if (target.kind === "Model" || target.kind === "ModelProperty") { - // Is it ultimately a numeric type? - if (isNumericType(program, target)) { - program.stateMap(maxValuesKey).set(target, maxValue); - } else { - throwDiagnostic("Cannot apply @maxValue to a non-numeric type", target); - } - } else { - throwDiagnostic( + if (target.kind !== "Model" && target.kind !== "ModelProperty") { + program.reportDiagnostic( "Cannot apply @maxValue to anything that isn't a Model or ModelProperty", target ); + return; } + if (!isNumericType(program, target)) { + program.reportDiagnostic("Cannot apply @maxValue to a non-numeric type", target); + return; + } + program.stateMap(maxValuesKey).set(target, maxValue); } export function getMaxValue(program: Program, target: Type): number | undefined { @@ -192,16 +197,16 @@ export function getMaxValue(program: Program, target: Type): number | undefined const secretTypesKey = Symbol(); export function secret(program: Program, target: Type) { - if (target.kind === "Model") { - // Is it a model type that ultimately derives from 'string'? - if (getIntrinsicType(program, target) === "string") { - program.stateMap(secretTypesKey).set(target, true); - } else { - throwDiagnostic("Cannot apply @secret to a non-string type", target); - } - } else { - throwDiagnostic("Cannot apply @secret to anything that isn't a Model", target); + if (target.kind !== "Model") { + program.reportDiagnostic("Cannot apply @secret to anything that isn't a Model", target); + return; } + + if (getIntrinsicType(program, target) !== "string") { + program.reportDiagnostic("Cannot apply @secret to a non-string type", target); + return; + } + program.stateMap(secretTypesKey).set(target, true); } export function isSecret(program: Program, target: Type): boolean | undefined { @@ -213,11 +218,14 @@ export function isSecret(program: Program, target: Type): boolean | undefined { const visibilitySettingsKey = Symbol(); export function visibility(program: Program, target: Type, ...visibilities: string[]) { - if (target.kind === "ModelProperty") { - program.stateMap(visibilitySettingsKey).set(target, visibilities); - } else { - throwDiagnostic("The @visibility decorator can only be applied to model properties.", target); + if (target.kind !== "ModelProperty") { + program.reportDiagnostic( + "The @visibility decorator can only be applied to model properties.", + target + ); + return; } + program.stateMap(visibilitySettingsKey).set(target, visibilities); } export function getVisibility(program: Program, target: Type): string[] | undefined { @@ -226,7 +234,11 @@ export function getVisibility(program: Program, target: Type): string[] | undefi export function withVisibility(program: Program, target: Type, ...visibilities: string[]) { if (target.kind !== "Model") { - throwDiagnostic("The @withVisibility decorator can only be applied to models.", target); + program.reportDiagnostic( + "The @withVisibility decorator can only be applied to models.", + target + ); + return; } const filter = (_: any, prop: ModelTypeProperty) => { @@ -253,14 +265,14 @@ function mapFilterOut( const listPropertiesKey = Symbol(); export function list(program: Program, target: Type) { - if (target.kind === "Operation" || target.kind === "ModelProperty") { - program.stateSet(listPropertiesKey).add(target); - } else { - throwDiagnostic( - "The @list decorator can only be applied to interface or model properties.", + if (target.kind !== "Operation" && target.kind !== "ModelProperty") { + program.reportDiagnostic( + "The @list decorator can only be applied to operations or model properties.", target ); + return; } + program.stateSet(listPropertiesKey).add(target); } export function isList(program: Program, target: Type): boolean { @@ -273,15 +285,18 @@ const tagPropertiesKey = Symbol(); // Set a tag on an operation or namespace. There can be multiple tags on either an // operation or namespace. export function tag(program: Program, target: Type, tag: string) { - if (target.kind === "Operation" || target.kind === "Namespace") { - const tags = program.stateMap(tagPropertiesKey).get(target); - if (tags) { - tags.push(tag); - } else { - program.stateMap(tagPropertiesKey).set(target, [tag]); - } + if (target.kind !== "Operation" && target.kind !== "Namespace") { + program.reportDiagnostic( + "The @tag decorator can only be applied to namespaces or operations.", + target + ); + return; + } + const tags = program.stateMap(tagPropertiesKey).get(target); + if (tags) { + tags.push(tag); } else { - throwDiagnostic("The @tag decorator can only be applied to namespace or operation.", target); + program.stateMap(tagPropertiesKey).set(target, [tag]); } } diff --git a/packages/adl/test/checker/check-parse-errors.ts b/packages/adl/test/checker/check-parse-errors.ts new file mode 100644 index 000000000..7e00df54b --- /dev/null +++ b/packages/adl/test/checker/check-parse-errors.ts @@ -0,0 +1,27 @@ +import { match, strictEqual } from "assert"; +import { createTestHost, TestHost } from "../test-host.js"; + +describe("adl: semantic checks on source with parse errors", () => { + let testHost: TestHost; + + beforeEach(async () => { + testHost = await createTestHost(); + }); + + it("reports semantic errors in addition to parse errors", async () => { + testHost.addAdlFile( + "a.adl", + `model M extends Q { + a: B; + a: C; + ` + ); + + const diagnostics = await testHost.diagnose("/"); + strictEqual(diagnostics.length, 4); + match(diagnostics[0].message, /Property expected/); + match(diagnostics[1].message, /Unknown identifier Q/); + match(diagnostics[2].message, /Unknown identifier B/); + match(diagnostics[3].message, /Unknown identifier C/); + }); +}); diff --git a/packages/adl/test/checker/duplicate-ids.ts b/packages/adl/test/checker/duplicate-ids.ts index 10e70dc3c..966f68167 100644 --- a/packages/adl/test/checker/duplicate-ids.ts +++ b/packages/adl/test/checker/duplicate-ids.ts @@ -1,4 +1,5 @@ -import { rejects } from "assert"; +import { match, strictEqual } from "assert"; +import { Diagnostic } from "../../compiler/types.js"; import { createTestHost, TestHost } from "../test-host.js"; describe("adl: duplicate declarations", () => { @@ -8,7 +9,7 @@ describe("adl: duplicate declarations", () => { testHost = await createTestHost(); }); - it("throws for duplicate template parameters", async () => { + it("reports duplicate template parameters", async () => { testHost.addAdlFile( "a.adl", ` @@ -16,10 +17,11 @@ describe("adl: duplicate declarations", () => { ` ); - await rejects(testHost.compile("/")); + const diagnostics = await testHost.diagnose("/"); + assertDuplicates(diagnostics); }); - it("throws for duplicate model declarations in global scope", async () => { + it("reports duplicate model declarations in global scope", async () => { testHost.addAdlFile( "a.adl", ` @@ -28,10 +30,11 @@ describe("adl: duplicate declarations", () => { ` ); - await rejects(testHost.compile("/")); + const diagnostics = await testHost.diagnose("/"); + assertDuplicates(diagnostics); }); - it("throws for duplicate model declarations in a single namespace", async () => { + it("reports duplicate model declarations in a single namespace", async () => { testHost.addAdlFile( "a.adl", ` @@ -41,10 +44,11 @@ describe("adl: duplicate declarations", () => { ` ); - await rejects(testHost.compile("/")); + const diagnostics = await testHost.diagnose("/"); + assertDuplicates(diagnostics); }); - it("throws for duplicate model declarations in across multiple namespaces", async () => { + it("reports duplicate model declarations across multiple namespaces", async () => { testHost.addAdlFile( "a.adl", ` @@ -58,10 +62,11 @@ describe("adl: duplicate declarations", () => { ` ); - await rejects(testHost.compile("/")); + const diagnostics = await testHost.diagnose("/"); + assertDuplicates(diagnostics); }); - it("throws for duplicate model declarations in across multiple files and namespaces", async () => { + it("reports duplicate model declarations across multiple files and namespaces", async () => { testHost.addAdlFile( "a.adl", ` @@ -79,6 +84,14 @@ describe("adl: duplicate declarations", () => { ` ); - await rejects(testHost.compile("/")); + const diagnostics = await testHost.diagnose("/"); + assertDuplicates(diagnostics); }); }); + +function assertDuplicates(diagnostics: readonly Diagnostic[]) { + strictEqual(diagnostics.length, 2); + for (const diagnostic of diagnostics) { + match(diagnostic.message, /Duplicate name/); + } +} diff --git a/packages/adl/test/config/config.ts b/packages/adl/test/config/config.ts index d51a7f632..bc98aa5f5 100644 --- a/packages/adl/test/config/config.ts +++ b/packages/adl/test/config/config.ts @@ -1,7 +1,6 @@ -import { deepStrictEqual, throws } from "assert"; +import { deepStrictEqual } from "assert"; import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; -import { DiagnosticError } from "../../compiler/diagnostics.js"; import { ConfigValidator } from "../../config/config-validator.js"; import { loadADLConfigInDir } from "../../config/index.js"; @@ -20,6 +19,7 @@ describe("adl: config file loading", () => { const config = await loadTestConfig(folderName); deepStrictEqual(config, { plugins: ["foo"], + diagnostics: [], emitters: { "foo:openapi": true, }, @@ -48,6 +48,7 @@ describe("adl: config file loading", () => { const config = await loadTestConfig("empty"); deepStrictEqual(config, { plugins: [], + diagnostics: [], emitters: {}, lint: { extends: [], @@ -62,17 +63,16 @@ describe("adl: config file loading", () => { }); it("deep clones defaults when not found", async () => { - // load and mutate let config = await loadTestConfig("empty"); config.plugins.push("x"); config.emitters["x"] = true; config.lint.extends.push("x"); config.lint.rules["x"] = "off"; - // reload and make sure mutation is not observed config = await loadTestConfig("empty"); deepStrictEqual(config, { plugins: [], + diagnostics: [], emitters: {}, lint: { extends: [], @@ -82,17 +82,16 @@ describe("adl: config file loading", () => { }); it("deep clones defaults when found", async () => { - // load and mutate let config = await loadTestConfig("yaml"); config.plugins.push("x"); config.emitters["x"] = true; config.lint.extends.push("x"); config.lint.rules["x"] = "off"; - // reload and make sure mutation is not observed config = await loadTestConfig("yaml"); deepStrictEqual(config, { plugins: ["foo"], + diagnostics: [], emitters: { "foo:openapi": true, }, @@ -110,32 +109,26 @@ describe("adl: config file loading", () => { const validator = new ConfigValidator(); it("does not allow additional properties", () => { - throws( - () => validator.validateConfig({ someCustomProp: true } as any), - new DiagnosticError([ - { - severity: "error", - message: - "Schema violation: must NOT have additional properties (/)\n additionalProperty: someCustomProp", - }, - ]) - ); + deepStrictEqual(validator.validateConfig({ someCustomProp: true } as any), [ + { + severity: "error", + message: + "Schema violation: must NOT have additional properties (/)\n additionalProperty: someCustomProp", + }, + ]); }); - it("fail if passing the wrong type", () => { - throws( - () => validator.validateConfig({ emitters: true } as any), - new DiagnosticError([ - { - severity: "error", - message: "Schema violation: must be object (/emitters)", - }, - ]) - ); + it("fails if passing the wrong type", () => { + deepStrictEqual(validator.validateConfig({ emitters: true } as any), [ + { + severity: "error", + message: "Schema violation: must be object (/emitters)", + }, + ]); }); - it("succeeed if config is valid", () => { - validator.validateConfig({ lint: { rules: { foo: "on" } } }); + it("succeeeds if config is valid", () => { + deepStrictEqual(validator.validateConfig({ lint: { rules: { foo: "on" } } }), []); }); }); }); diff --git a/packages/adl/test/test-host.ts b/packages/adl/test/test-host.ts index 758e38f4a..290704aeb 100644 --- a/packages/adl/test/test-host.ts +++ b/packages/adl/test/test-host.ts @@ -1,10 +1,10 @@ import { readdir, readFile } from "fs/promises"; import { basename, isAbsolute, join, normalize, relative, resolve, sep } from "path"; import { fileURLToPath, pathToFileURL } from "url"; -import { CompilerOptions } from "../compiler/options"; -import { Program } from "../compiler/program"; -import { createProgram } from "../compiler/program.js"; -import { CompilerHost, Type } from "../compiler/types"; +import { formatDiagnostic, logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js"; +import { CompilerOptions } from "../compiler/options.js"; +import { createProgram, Program } from "../compiler/program.js"; +import { CompilerHost, Diagnostic, Type } from "../compiler/types.js"; export interface TestHost { addAdlFile(path: string, contents: string): void; @@ -12,6 +12,11 @@ export interface TestHost { addRealAdlFile(path: string, realPath: string): Promise; addRealJsFile(path: string, realPath: string): Promise; compile(main: string, options?: CompilerOptions): Promise>; + diagnose(main: string, options?: CompilerOptions): Promise; + compileAndDiagnose( + main: string, + options?: CompilerOptions + ): Promise<[Record, readonly Diagnostic[]]>; testTypes: Record; program: Program; /** @@ -146,6 +151,8 @@ export async function createTestHost(): Promise { addRealAdlFile, addRealJsFile, compile, + diagnose, + compileAndDiagnose, testTypes, get program() { return program; @@ -177,24 +184,34 @@ export async function createTestHost(): Promise { } async function compile(main: string, options: CompilerOptions = {}) { + const [testTypes, diagnostics] = await compileAndDiagnose(main, options); + if (diagnostics.length > 0) { + let message = "Unexpected diagnostics:\n" + diagnostics.map(formatDiagnostic).join("\n"); + throw new Error(message); + } + return testTypes; + } + + async function diagnose(main: string, options: CompilerOptions = {}) { + const [, diagnostics] = await compileAndDiagnose(main, options); + return diagnostics; + } + + async function compileAndDiagnose( + main: string, + options: CompilerOptions = {} + ): Promise<[Record, readonly Diagnostic[]]> { // default is noEmit if (!options.hasOwnProperty("noEmit")) { options.noEmit = true; } - try { - program = await createProgram(compilerHost, { - mainFile: main, - ...options, - }); - - return testTypes; - } catch (e) { - if (e.diagnostics) { - throw e.diagnostics; - } - throw e; - } + program = await createProgram(compilerHost, { + mainFile: main, + ...options, + }); + logVerboseTestOutput((log) => logDiagnostics(program.diagnostics, log)); + return [testTypes, program.diagnostics]; } function isContainedIn(a: string, b: string) { diff --git a/packages/adl/test/test-parser.ts b/packages/adl/test/test-parser.ts index d0cb09577..9553c1d06 100644 --- a/packages/adl/test/test-parser.ts +++ b/packages/adl/test/test-parser.ts @@ -1,6 +1,6 @@ import assert from "assert"; import { CharCode } from "../compiler/charcode.js"; -import { logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js"; +import { formatDiagnostic, logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js"; import { hasParseError, NodeFlags, parse } from "../compiler/parser.js"; import { ADLScriptNode, SyntaxKind } from "../compiler/types.js"; @@ -435,17 +435,15 @@ function parseEach(cases: (string | [string, Callback])[]) { logVerboseTestOutput("\n=== Diagnostics ==="); if (astNode.parseDiagnostics.length > 0) { - const diagnostics: string[] = []; - logDiagnostics(astNode.parseDiagnostics, (e) => diagnostics.push(e!)); + const diagnostics = astNode.parseDiagnostics.map(formatDiagnostic).join("\n"); assert.strictEqual( hasParseError(astNode), astNode.parseDiagnostics.some((e) => e.severity === "error"), - "root node claims to have no parse errors, but these were reported:\n" + - diagnostics.join("\n") + "root node claims to have no parse errors, but these were reported:\n" + diagnostics ); - assert.fail("Unexpected parse errors in test:\n" + diagnostics.join("\n")); + assert.fail("Unexpected parse errors in test:\n" + diagnostics); } }); } diff --git a/packages/adl/test/test-scanner.ts b/packages/adl/test/test-scanner.ts index 69c661f60..ec127fea9 100644 --- a/packages/adl/test/test-scanner.ts +++ b/packages/adl/test/test-scanner.ts @@ -2,7 +2,7 @@ import assert from "assert"; import { readFile } from "fs/promises"; import { URL } from "url"; import { isIdentifierContinue, isIdentifierStart } from "../compiler/charcode.js"; -import { createDiagnostic, formatDiagnostic, throwOnError } from "../compiler/diagnostics.js"; +import { DiagnosticHandler, formatDiagnostic } from "../compiler/diagnostics.js"; import { createScanner, isKeyword, @@ -25,8 +25,13 @@ type TokenEntry = [ }? ]; -function tokens(text: string, onError = throwOnError): TokenEntry[] { - const scanner = createScanner(text, onError); +function tokens(text: string, diagnosticHandler?: DiagnosticHandler): TokenEntry[] { + if (!diagnosticHandler) { + diagnosticHandler = (diagnostic) => + assert.fail("Unexpected diagnostic: " + formatDiagnostic(diagnostic)); + } + + const scanner = createScanner(text, diagnosticHandler); const result: TokenEntry[] = []; do { const token = scanner.scan(); @@ -181,8 +186,7 @@ describe("adl: scanner", () => { }); function scanString(text: string, expectedValue: string, expectedDiagnostic?: RegExp) { - const scanner = createScanner(text, (message, target, args) => { - const diagnostic = createDiagnostic(message, target, args); + const scanner = createScanner(text, (diagnostic) => { if (expectedDiagnostic) { assert.match(diagnostic.message, expectedDiagnostic); } else { From 8518a0bd17a70c04fe6a14198f0b2385450a7c26 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Fri, 4 Jun 2021 14:12:59 -0700 Subject: [PATCH 41/47] Use same entry point handling for compile and import (#577) We no longer glob files when a directory is passed to `adl compile`. Instead, we will look for `adlMain` specified by package.json or main.adl in the directory. This eliminates any dependency on file system enumeration order and will make it easier for tooling to implement semantic features. Also: * Fix various issues with reporting I/O errors as diagnostics. * Fix issue where regen-samples would silently pass PR validation if nothing at all was emitted. * Make regen-samples have opt-out exclude list instead of opt-in include list. * Report correct location for syntax errors in decorators, and fix issue with copying petstore sample to another folder without type: module in package.json * Allow .mjs extension for decorator imports. * Make sure unahndled promise rejection becomes internal compiler error. * Fix issue with dumping config with diagnostics from `adl info` * Avoid repeating config filename in `adl info` --- packages/adl/compiler/cli.ts | 110 +++++++++--------- packages/adl/compiler/messages.ts | 2 +- packages/adl/compiler/options.ts | 1 - packages/adl/compiler/program.ts | 101 ++++++++-------- packages/adl/compiler/types.ts | 2 +- packages/adl/compiler/util.ts | 79 ++++++++++++- packages/adl/config/config-loader.ts | 59 +++++----- packages/adl/config/config-validator.ts | 23 ++-- packages/adl/lib/main.adl | 2 + packages/adl/test/checker/alias.ts | 12 +- .../adl/test/checker/check-parse-errors.ts | 2 +- packages/adl/test/checker/duplicate-ids.ts | 15 ++- packages/adl/test/checker/enum.ts | 6 +- packages/adl/test/checker/loader.ts | 4 +- packages/adl/test/checker/namespaces.ts | 58 +++++++-- packages/adl/test/checker/spread.ts | 3 +- packages/adl/test/checker/using.ts | 49 ++++++++ packages/adl/test/config/config.ts | 21 +++- packages/adl/test/decorators/range-limits.ts | 2 +- packages/adl/test/libraries/test-libraries.ts | 5 +- packages/adl/test/test-host.ts | 94 ++++++++------- 21 files changed, 423 insertions(+), 227 deletions(-) create mode 100644 packages/adl/lib/main.adl diff --git a/packages/adl/compiler/cli.ts b/packages/adl/compiler/cli.ts index c3cd1d11b..bf1d314cf 100644 --- a/packages/adl/compiler/cli.ts +++ b/packages/adl/compiler/cli.ts @@ -16,10 +16,10 @@ const args = yargs(process.argv.slice(2)) .scriptName("adl") .help() .strict() - .command("compile ", "Compile a directory of ADL files.", (cmd) => { + .command("compile ", "Compile ADL source.", (cmd) => { return cmd .positional("path", { - description: "The path to folder containing .adl files", + description: "The path to the main.adl file or directory containing main.adl.", type: "string", }) .option("output-path", { @@ -40,44 +40,40 @@ const args = yargs(process.argv.slice(2)) describe: "Don't load the ADL standard library.", }); }) - .command( - "generate ", - "Generate client and server code from a directory of ADL files.", - (cmd) => { - return ( - cmd - .positional("path", { - description: "The path to folder containing .adl files", - type: "string", - }) - .option("client", { - type: "boolean", - describe: "Generate a client library for the ADL definition", - }) - .option("language", { - type: "string", - choices: ["typescript", "csharp", "python"], - describe: "The language to use for code generation", - }) - .option("output-path", { - type: "string", - default: "./adl-output", - describe: - "The output path for generated artifacts. If it does not exist, it will be created.", - }) - .option("option", { - type: "array", - string: true, - describe: - "Key/value pairs that can be passed to ADL components. The format is 'key=value'. This parameter can be used multiple times to add more options.", - }) - // we can't generate anything but a client yet - .demandOption("client") - // and language is required to do so - .demandOption("language") - ); - } - ) + .command("generate ", "Generate client code from ADL source.", (cmd) => { + return ( + cmd + .positional("path", { + description: "The path to folder containing .adl files", + type: "string", + }) + .option("client", { + type: "boolean", + describe: "Generate a client library for the ADL definition", + }) + .option("language", { + type: "string", + choices: ["typescript", "csharp", "python"], + describe: "The language to use for code generation", + }) + .option("output-path", { + type: "string", + default: "./adl-output", + describe: + "The output path for generated artifacts. If it does not exist, it will be created.", + }) + .option("option", { + type: "array", + string: true, + describe: + "Key/value pairs that can be passed to ADL components. The format is 'key=value'. This parameter can be used multiple times to add more options.", + }) + // we can't generate anything but a client yet + .demandOption("client") + // and language is required to do so + .demandOption("language") + ); + }) .command("code", "Manage VS Code Extension.", (cmd) => { return cmd .demandCommand(1, "No command specified.") @@ -261,9 +257,12 @@ async function printInfo() { const config = await loadADLConfigInDir(cwd); const jsyaml = await import("js-yaml"); + const excluded = ["diagnostics", "filename"]; + const replacer = (key: string, value: any) => (excluded.includes(key) ? undefined : value); + console.log(`User Config: ${config.filename ?? "No config file found"}`); console.log("-----------"); - console.log(jsyaml.dump(config)); + console.log(jsyaml.dump(config, { replacer })); console.log("-----------"); logDiagnostics(config.diagnostics, console.error); if (config.diagnostics.some((d) => d.severity === "error")) { @@ -377,15 +376,20 @@ async function main() { } } -main() - .then(() => {}) - .catch((err) => { - // NOTE: An expected error, like one thrown for bad input, shouldn't reach - // here, but be handled somewhere else. If we reach here, it should be - // considered a bug and therefore we should not suppress the stack trace as - // that risks losing it in the case of a bug that does not repro easily. - console.error("Internal compiler error!"); - console.error("File issue at https://github.com/azure/adl"); - dumpError(err, console.error); - process.exit(1); - }); +function internalCompilerError(error: Error) { + // NOTE: An expected error, like one thrown for bad input, shouldn't reach + // here, but be handled somewhere else. If we reach here, it should be + // considered a bug and therefore we should not suppress the stack trace as + // that risks losing it in the case of a bug that does not repro easily. + console.error("Internal compiler error!"); + console.error("File issue at https://github.com/azure/adl"); + dumpError(error, console.error); + process.exit(1); +} + +process.on("unhandledRejection", (error: Error) => { + console.error("Unhandled promise rejection!"); + internalCompilerError(error); +}); + +main().catch(internalCompilerError); diff --git a/packages/adl/compiler/messages.ts b/packages/adl/compiler/messages.ts index 5b0f23b4f..4bee35a4a 100644 --- a/packages/adl/compiler/messages.ts +++ b/packages/adl/compiler/messages.ts @@ -62,7 +62,7 @@ export const Message = { FileNotFound: { code: 1109, - text: `File {0} is not found.`, + text: `File {0} not found.`, severity: "error", } as const, }; diff --git a/packages/adl/compiler/options.ts b/packages/adl/compiler/options.ts index 3e20b454b..8c223b019 100644 --- a/packages/adl/compiler/options.ts +++ b/packages/adl/compiler/options.ts @@ -1,6 +1,5 @@ export interface CompilerOptions { miscOptions?: any; - mainFile?: string; outputPath?: string; swaggerOutputFile?: string; nostdlib?: boolean; diff --git a/packages/adl/compiler/program.ts b/packages/adl/compiler/program.ts index 179a8ee8e..4a0452175 100644 --- a/packages/adl/compiler/program.ts +++ b/packages/adl/compiler/program.ts @@ -1,4 +1,4 @@ -import { dirname, extname, isAbsolute, join, resolve } from "path"; +import { dirname, extname, isAbsolute, resolve } from "path"; import resolveModule from "resolve"; import { createBinder, createSymbolTable } from "./binder.js"; import { createChecker } from "./checker.js"; @@ -22,6 +22,7 @@ import { SyntaxKind, Type, } from "./types.js"; +import { doIO, loadFile } from "./util.js"; export interface Program { compilerOptions: CompilerOptions; @@ -52,7 +53,8 @@ export interface Program { export async function createProgram( host: CompilerHost, - options: CompilerOptions + mainFile: string, + options: CompilerOptions = {} ): Promise { const buildCbs: any = []; const stateMaps = new Map>(); @@ -63,7 +65,7 @@ export async function createProgram( let error = false; const program: Program = { - compilerOptions: options || {}, + compilerOptions: options, globalNamespace: createGlobalNamespace(), sourceFiles: [], literalTypes: new Map(), @@ -94,7 +96,7 @@ export async function createProgram( await loadStandardLibrary(program); } - await loadMain(options); + await loadMain(mainFile, options); const checker = (program.checker = createChecker(program)); program.checker.checkProgram(program); @@ -187,36 +189,40 @@ export async function createProgram( } } - async function loadDirectory(rootDir: string) { - const dir = await host.readDir(rootDir); - for (const entry of dir) { - if (entry.isFile()) { - const path = join(rootDir, entry.name); - if (entry.name.endsWith(".js")) { - await loadJsFile(path); - } else if (entry.name.endsWith(".adl")) { - await loadAdlFile(path); - } - } - } + async function loadDirectory(dir: string, diagnosticTarget?: DiagnosticTarget) { + const pkgJsonPath = resolve(dir, "package.json"); + let [pkg] = await loadFile(host.readFile, pkgJsonPath, JSON.parse, program.reportDiagnostic, { + allowFileNotFound: true, + diagnosticTarget, + }); + const mainFile = resolve(dir, pkg?.adlMain ?? "main.adl"); + await loadAdlFile(mainFile, diagnosticTarget); } - async function loadAdlFile(path: string) { + async function loadAdlFile(path: string, diagnosticTarget?: DiagnosticTarget) { if (seenSourceFiles.has(path)) { return; } seenSourceFiles.add(path); - const contents = await host.readFile(path); - if (!contents) { - throw new Error("Couldn't load ADL file " + path); - } + const contents = await doIO(host.readFile, path, program.reportDiagnostic, { + diagnosticTarget, + }); - await evalAdlScript(contents, path); + if (contents) { + await evalAdlScript(contents, path); + } } - async function loadJsFile(path: string) { - const exports = await host.getJsImport(path); + async function loadJsFile(path: string, diagnosticTarget: DiagnosticTarget) { + const exports = await doIO(host.getJsImport, path, program.reportDiagnostic, { + diagnosticTarget, + jsDiagnosticTarget: { file: createSourceFile("", path), pos: 0, end: 0 }, + }); + + if (!exports) { + return; + } for (const match of Object.keys(exports)) { // bind JS files early since this is the only work @@ -279,12 +285,11 @@ export async function createProgram( const ext = extname(target); if (ext === "") { - // look for a main.adl - await loadAdlFile(join(target, "main.adl")); - } else if (ext === ".js") { - await loadJsFile(target); + await loadDirectory(target, stmt); + } else if (ext === ".js" || ext === ".mjs") { + await loadJsFile(target, stmt); } else if (ext === ".adl") { - await loadAdlFile(target); + await loadAdlFile(target, stmt); } else { program.reportDiagnostic( "Import paths must reference either a directory, a .adl file, or .js file", @@ -342,7 +347,13 @@ export async function createProgram( host .realpath(path) .then((p) => cb(null, p)) - .catch((e) => cb(e)); + .catch((e) => { + if (e.code === "ENOENT" || e.code === "ENOTDIR") { + cb(null, path); + } else { + cb(e); + } + }); }, packageFilter(pkg) { // this lets us follow node resolve semantics more-or-less exactly @@ -355,8 +366,7 @@ export async function createProgram( if (err) { rejectP(err); } else if (!resolved) { - // I don't know when this happens - rejectP(new Error("Couldn't resolve module")); + rejectP(new Error("BUG: Module resolution succeeded but didn't return a value.")); } else { resolveP(resolved); } @@ -365,14 +375,9 @@ export async function createProgram( }); } - async function loadMain(options: CompilerOptions) { - if (!options.mainFile) { - throw new Error("Must specify a main file"); - } - - const mainPath = resolve(host.getCwd(), options.mainFile); - - const mainStat = await getMainPathStats(mainPath); + async function loadMain(mainFile: string, options: CompilerOptions) { + const mainPath = resolve(host.getCwd(), mainFile); + const mainStat = await doIO(host.stat, mainPath, program.reportDiagnostic); if (!mainStat) { return; } @@ -383,18 +388,6 @@ export async function createProgram( } } - async function getMainPathStats(mainPath: string) { - try { - return await host.stat(mainPath); - } catch (e) { - if (e.code === "ENOENT") { - program.reportDiagnostic(Message.FileNotFound, NoTarget, [mainPath]); - return undefined; - } - throw e; - } - } - function getOption(key: string): string | undefined { return (options.miscOptions || {})[key]; } @@ -458,9 +451,9 @@ export async function createProgram( } export async function compile( - rootDir: string, + mainFile: string, host: CompilerHost, options?: CompilerOptions ): Promise { - return await createProgram(host, { mainFile: rootDir, ...options }); + return await createProgram(host, mainFile, options); } diff --git a/packages/adl/compiler/types.ts b/packages/adl/compiler/types.ts index c4f4cf685..71f4052c6 100644 --- a/packages/adl/compiler/types.ts +++ b/packages/adl/compiler/types.ts @@ -525,7 +525,7 @@ export interface Dirent { export interface CompilerHost { // read a utf-8 encoded file - readFile(path: string): Promise; + readFile(path: string): Promise; // read the contents of a directory readDir(path: string): Promise; diff --git a/packages/adl/compiler/util.ts b/packages/adl/compiler/util.ts index 46f9e6b2d..6b48ab1d5 100644 --- a/packages/adl/compiler/util.ts +++ b/packages/adl/compiler/util.ts @@ -2,7 +2,15 @@ import fs from "fs"; import { readdir, readFile, realpath, stat, writeFile } from "fs/promises"; import { join, resolve } from "path"; import { fileURLToPath, pathToFileURL, URL } from "url"; -import { CompilerHost } from "./types.js"; +import { + createDiagnostic, + createSourceFile, + DiagnosticHandler, + DiagnosticTarget, + Message, + NoTarget, +} from "./diagnostics.js"; +import { CompilerHost, SourceFile } from "./types.js"; export const adlVersion = getVersion(); @@ -40,6 +48,73 @@ export function deepClone(value: T): T { return value; } +export interface FileHandlingOptions { + allowFileNotFound?: boolean; + diagnosticTarget?: DiagnosticTarget; + jsDiagnosticTarget?: DiagnosticTarget; +} + +export async function doIO( + action: (path: string) => Promise, + path: string, + reportDiagnostic: DiagnosticHandler, + options?: FileHandlingOptions +): Promise { + let result; + try { + result = await action(path); + } catch (e) { + let diagnostic; + let target = options?.diagnosticTarget ?? NoTarget; + + // blame the JS file, not the ADL import statement for JS syntax errors. + if (e instanceof SyntaxError && options?.jsDiagnosticTarget) { + target = options.jsDiagnosticTarget; + } + + switch (e.code) { + case "ENOENT": + if (options?.allowFileNotFound) { + return undefined; + } + diagnostic = createDiagnostic(Message.FileNotFound, target, [path]); + break; + default: + diagnostic = createDiagnostic(e.message, target); + break; + } + + reportDiagnostic(diagnostic); + return undefined; + } + + return result; +} + +export async function loadFile( + read: (path: string) => Promise, + path: string, + load: (contents: string) => T, + reportDiagnostic: DiagnosticHandler, + options?: FileHandlingOptions +): Promise<[T | undefined, SourceFile]> { + const contents = await doIO(read, path, reportDiagnostic, options); + if (!contents) { + return [undefined, createSourceFile("", path)]; + } + + const file = createSourceFile(contents, path); + let data: T; + try { + data = load(contents); + } catch (e) { + reportDiagnostic({ message: e.message, severity: "error", file }); + return [undefined, file]; + } + + return [data, file]; +} + export const NodeHost: CompilerHost = { readFile: (path: string) => readFile(path, "utf-8"), readDir: (path: string) => readdir(path, { withFileTypes: true }), @@ -49,7 +124,7 @@ export const NodeHost: CompilerHost = { getJsImport: (path: string) => import(pathToFileURL(path).href), getLibDirs() { const rootDir = this.getExecutionRoot(); - return [join(rootDir, "lib"), join(rootDir, "dist/lib")]; + return [join(rootDir, "lib")]; }, stat(path: string) { return stat(path); diff --git a/packages/adl/config/config-loader.ts b/packages/adl/config/config-loader.ts index 5967b5419..054bd00f8 100644 --- a/packages/adl/config/config-loader.ts +++ b/packages/adl/config/config-loader.ts @@ -1,8 +1,8 @@ import { readFile } from "fs/promises"; import { basename, extname, join } from "path"; -import { createDiagnostic, createSourceFile } from "../compiler/diagnostics.js"; +import { Message } from "../compiler/diagnostics.js"; import { Diagnostic } from "../compiler/types.js"; -import { deepClone, deepFreeze } from "../compiler/util.js"; +import { deepClone, deepFreeze, loadFile } from "../compiler/util.js"; import { ConfigValidator } from "./config-validator.js"; import { ADLConfig } from "./types.js"; @@ -24,14 +24,14 @@ const defaultConfig: ADLConfig = deepFreeze({ export async function loadADLConfigInDir(directoryPath: string): Promise { for (const filename of configFilenames) { const filePath = join(directoryPath, filename); - try { - return await loadADLConfigFile(filePath); - } catch (e) { - if (e.code === "ENOENT") { - continue; - } - throw e; + const config = await loadADLConfigFile(filePath); + if ( + config.diagnostics.length === 1 && + config.diagnostics[0].code === Message.FileNotFound.code + ) { + continue; } + return config; } return deepClone(defaultConfig); } @@ -78,35 +78,32 @@ async function loadConfigFile( filePath: string, loadData: (content: string) => any ): Promise { - const content = await readFile(filePath, "utf-8"); - const file = createSourceFile(content, filePath); + const diagnostics: Diagnostic[] = []; + const reportDiagnostic = (d: Diagnostic) => diagnostics.push(d); - let loadDiagnostics: Diagnostic[]; - let data: any; - try { - data = loadData(content); - loadDiagnostics = []; - } catch (e) { - loadDiagnostics = [createDiagnostic(e.message, { file, pos: 0, end: 0 })]; + let [data, file] = await loadFile( + (path) => readFile(path, "utf-8"), + filePath, + loadData, + reportDiagnostic + ); + + if (data) { + configValidator.validateConfig(data, file, reportDiagnostic); } - const validationDiagnostics = configValidator.validateConfig(data); - const diagnostics = [...loadDiagnostics, ...validationDiagnostics]; - - if (diagnostics.some((d) => d.severity === "error")) { - // NOTE: Don't trust the data if there are validation errors, and use - // default config. Otherwise, we may return an object that does not - // conform to ADLConfig's typing. - data = defaultConfig; + if (!data || diagnostics.length > 0) { + // NOTE: Don't trust the data if there are errors and use default + // config. Otherwise, we may return an object that does not conform to + // ADLConfig's typing. + data = deepClone(defaultConfig); } else { mergeDefaults(data, defaultConfig); } - return { - ...data, - filename: filePath, - diagnostics, - }; + data.filename = filePath; + data.diagnostics = diagnostics; + return data; } /** diff --git a/packages/adl/config/config-validator.ts b/packages/adl/config/config-validator.ts index 44cef12b1..030b3913c 100644 --- a/packages/adl/config/config-validator.ts +++ b/packages/adl/config/config-validator.ts @@ -1,5 +1,5 @@ import Ajv, { ErrorObject } from "ajv"; -import { compilerAssert } from "../compiler/diagnostics.js"; +import { compilerAssert, DiagnosticHandler } from "../compiler/diagnostics.js"; import { Diagnostic, SourceFile } from "../compiler/types.js"; import { ADLConfigJsonSchema } from "./config-schema.js"; import { ADLRawConfig } from "./types.js"; @@ -15,20 +15,28 @@ export class ConfigValidator { * @param file @optional file for errors tracing. * @returns Validation */ - public validateConfig(config: ADLRawConfig, file?: SourceFile): Diagnostic[] { + public validateConfig( + config: ADLRawConfig, + file: SourceFile, + reportDiagnostic: DiagnosticHandler + ): void { const validate = this.ajv.compile(ADLConfigJsonSchema); const valid = validate(config); compilerAssert( !valid || !validate.errors, "There should be errors reported if the config file is not valid." ); - return validate.errors?.map((e) => ajvErrorToDiagnostic(e, file)) ?? []; + + for (const error of validate.errors ?? []) { + const diagnostic = ajvErrorToDiagnostic(error, file); + reportDiagnostic(diagnostic); + } } } const IGNORED_AJV_PARAMS = new Set(["type", "errors"]); -function ajvErrorToDiagnostic(error: ErrorObject, file?: SourceFile): Diagnostic { +function ajvErrorToDiagnostic(error: ErrorObject, file: SourceFile): Diagnostic { const messageLines = [`Schema violation: ${error.message} (${error.instancePath || "/"})`]; for (const [name, value] of Object.entries(error.params).filter( ([name]) => !IGNORED_AJV_PARAMS.has(name) @@ -37,9 +45,6 @@ function ajvErrorToDiagnostic(error: ErrorObject, file?: SourceFile): Diagnostic messageLines.push(` ${name}: ${formattedValue}`); } - return { - severity: "error", - message: messageLines.join("\n"), - ...(file && { file }), - }; + const message = messageLines.join("\n"); + return { message, severity: "error", file }; } diff --git a/packages/adl/lib/main.adl b/packages/adl/lib/main.adl new file mode 100644 index 000000000..09d7c9b9c --- /dev/null +++ b/packages/adl/lib/main.adl @@ -0,0 +1,2 @@ +import "../dist/lib/decorators.js"; +import "./lib.adl"; diff --git a/packages/adl/test/checker/alias.ts b/packages/adl/test/checker/alias.ts index 4497d5c07..d68a3d65a 100644 --- a/packages/adl/test/checker/alias.ts +++ b/packages/adl/test/checker/alias.ts @@ -11,7 +11,7 @@ describe("adl: aliases", () => { it("can alias a union expression", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` alias Foo = int32 | string; alias Bar = "hi" | 10; @@ -37,7 +37,7 @@ describe("adl: aliases", () => { it("can alias a deep union expression", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` alias Foo = int32 | string; alias Bar = "hi" | 10; @@ -65,7 +65,7 @@ describe("adl: aliases", () => { it("can alias a union expression with parameters", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` alias Foo = int32 | T; @@ -88,7 +88,7 @@ describe("adl: aliases", () => { it("can alias a deep union expression with parameters", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` alias Foo = int32 | T; alias Bar = Foo | Foo; @@ -114,7 +114,7 @@ describe("adl: aliases", () => { it("can alias an intersection expression", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` alias Foo = {a: string} & {b: string}; alias Bar = {c: string} & {d: string}; @@ -140,7 +140,7 @@ describe("adl: aliases", () => { it("can be used like any model", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` @test model Test { a: string }; diff --git a/packages/adl/test/checker/check-parse-errors.ts b/packages/adl/test/checker/check-parse-errors.ts index 7e00df54b..4c3403147 100644 --- a/packages/adl/test/checker/check-parse-errors.ts +++ b/packages/adl/test/checker/check-parse-errors.ts @@ -10,7 +10,7 @@ describe("adl: semantic checks on source with parse errors", () => { it("reports semantic errors in addition to parse errors", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", `model M extends Q { a: B; a: C; diff --git a/packages/adl/test/checker/duplicate-ids.ts b/packages/adl/test/checker/duplicate-ids.ts index 966f68167..393dcc6cb 100644 --- a/packages/adl/test/checker/duplicate-ids.ts +++ b/packages/adl/test/checker/duplicate-ids.ts @@ -11,7 +11,7 @@ describe("adl: duplicate declarations", () => { it("reports duplicate template parameters", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` model A { } ` @@ -23,7 +23,7 @@ describe("adl: duplicate declarations", () => { it("reports duplicate model declarations in global scope", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` model A { } model A { } @@ -36,7 +36,7 @@ describe("adl: duplicate declarations", () => { it("reports duplicate model declarations in a single namespace", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` namespace Foo; model A { } @@ -50,7 +50,7 @@ describe("adl: duplicate declarations", () => { it("reports duplicate model declarations across multiple namespaces", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` namespace N { model A { }; @@ -67,6 +67,13 @@ describe("adl: duplicate declarations", () => { }); it("reports duplicate model declarations across multiple files and namespaces", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` diff --git a/packages/adl/test/checker/enum.ts b/packages/adl/test/checker/enum.ts index a2b782163..243a9d9b3 100644 --- a/packages/adl/test/checker/enum.ts +++ b/packages/adl/test/checker/enum.ts @@ -11,7 +11,7 @@ describe("adl: enums", () => { it("can be valueless", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` @test enum E { A, B, C @@ -31,7 +31,7 @@ describe("adl: enums", () => { it("can have values", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` @test enum E { @test("A") A: "a"; @@ -56,7 +56,7 @@ describe("adl: enums", () => { it("can be a model property", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` namespace Foo; enum E { A, B, C } diff --git a/packages/adl/test/checker/loader.ts b/packages/adl/test/checker/loader.ts index 893aa54fd..4104649e9 100644 --- a/packages/adl/test/checker/loader.ts +++ b/packages/adl/test/checker/loader.ts @@ -10,7 +10,7 @@ describe("adl: loader", () => { it("loads ADL and JS files", async () => { testHost.addJsFile("blue.js", { blue() {} }); testHost.addAdlFile( - "a.adl", + "main.adl", ` import "./b.adl"; import "./blue.js"; @@ -39,6 +39,6 @@ describe("adl: loader", () => { ` ); - await testHost.compile("a.adl"); + await testHost.compile("main.adl"); }); }); diff --git a/packages/adl/test/checker/namespaces.ts b/packages/adl/test/checker/namespaces.ts index f744d84a6..74df2269f 100644 --- a/packages/adl/test/checker/namespaces.ts +++ b/packages/adl/test/checker/namespaces.ts @@ -17,8 +17,9 @@ describe("adl: namespaces with blocks", () => { it("can be decorated", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` + import "./blue.js"; @blue @test namespace Z.Q; @blue @test namespace N { } @blue @test namespace X.Y { } @@ -37,7 +38,7 @@ describe("adl: namespaces with blocks", () => { it("merges like namespaces", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` @test namespace N { @test model X { x: string } } @@ -58,6 +59,14 @@ describe("adl: namespaces with blocks", () => { }); it("merges like namespaces across files", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + import "./c.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` @@ -90,6 +99,14 @@ describe("adl: namespaces with blocks", () => { }); it("merges sub-namespaces across files", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + import "./c.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` @@ -117,7 +134,7 @@ describe("adl: namespaces with blocks", () => { it("can see things in outer scope same file", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` model A { } namespace N { model B extends A { } } @@ -127,6 +144,14 @@ describe("adl: namespaces with blocks", () => { }); it("can see things in outer scope cross file", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + import "./c.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` @@ -153,7 +178,7 @@ describe("adl: namespaces with blocks", () => { it("accumulates declarations inside of it", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` @test namespace Foo { namespace Bar { }; @@ -163,7 +188,7 @@ describe("adl: namespaces with blocks", () => { ` ); - const { Foo } = (await testHost.compile("/a.adl")) as { + const { Foo } = (await testHost.compile("./")) as { Foo: NamespaceType; }; @@ -187,6 +212,14 @@ describe("adl: blockless namespaces", () => { }); it("merges properly with other namespaces", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + import "./c.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` @@ -215,7 +248,7 @@ describe("adl: blockless namespaces", () => { it("does lookup correctly", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` namespace Repro; model Yo { @@ -235,7 +268,7 @@ describe("adl: blockless namespaces", () => { it("does lookup correctly with nested namespaces", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` namespace Repro; model Yo { @@ -265,7 +298,7 @@ describe("adl: blockless namespaces", () => { it("binds correctly", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` namespace N.M; model A { } @@ -287,7 +320,7 @@ describe("adl: blockless namespaces", () => { it("works with blockful namespaces", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` @test namespace N; @@ -314,6 +347,13 @@ describe("adl: blockless namespaces", () => { }); it("works with nested blockless and blockfull namespaces", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` diff --git a/packages/adl/test/checker/spread.ts b/packages/adl/test/checker/spread.ts index 8243d9cc3..f0e4a86a1 100644 --- a/packages/adl/test/checker/spread.ts +++ b/packages/adl/test/checker/spread.ts @@ -17,8 +17,9 @@ describe("adl: spread", () => { it("clones decorated properties", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` + import "./blue.js"; model A { @blue foo: string } model B { @blue bar: string } @test model C { ... A, ... B } diff --git a/packages/adl/test/checker/using.ts b/packages/adl/test/checker/using.ts index fc5d56af3..9d6b69944 100644 --- a/packages/adl/test/checker/using.ts +++ b/packages/adl/test/checker/using.ts @@ -10,6 +10,13 @@ describe("adl: using statements", () => { }); it("works in global scope", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` @@ -33,6 +40,13 @@ describe("adl: using statements", () => { }); it("works in namespaces", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` @@ -57,6 +71,13 @@ describe("adl: using statements", () => { }); it("works with dotted namespaces", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` @@ -80,6 +101,13 @@ describe("adl: using statements", () => { }); it("throws errors for duplicate imported usings", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` @@ -101,6 +129,13 @@ describe("adl: using statements", () => { }); it("throws errors for different usings with the same bindings", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` @@ -126,6 +161,13 @@ describe("adl: using statements", () => { }); it("resolves 'local' decls over usings", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` @@ -151,6 +193,13 @@ describe("adl: using statements", () => { }); it("usings are local to a file", async () => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + ` + ); testHost.addAdlFile( "a.adl", ` diff --git a/packages/adl/test/config/config.ts b/packages/adl/test/config/config.ts index bc98aa5f5..d90548b29 100644 --- a/packages/adl/test/config/config.ts +++ b/packages/adl/test/config/config.ts @@ -1,8 +1,10 @@ import { deepStrictEqual } from "assert"; import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; +import { createSourceFile } from "../../compiler/diagnostics.js"; +import { Diagnostic } from "../../compiler/types.js"; import { ConfigValidator } from "../../config/config-validator.js"; -import { loadADLConfigInDir } from "../../config/index.js"; +import { ADLRawConfig, loadADLConfigInDir } from "../../config/index.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -107,10 +109,18 @@ describe("adl: config file loading", () => { describe("validation", () => { const validator = new ConfigValidator(); + const file = createSourceFile("", ""); + + function validate(data: ADLRawConfig) { + const diagnostics: Diagnostic[] = []; + validator.validateConfig(data, file, (d) => diagnostics.push(d)); + return diagnostics; + } it("does not allow additional properties", () => { - deepStrictEqual(validator.validateConfig({ someCustomProp: true } as any), [ + deepStrictEqual(validate({ someCustomProp: true } as any), [ { + file, severity: "error", message: "Schema violation: must NOT have additional properties (/)\n additionalProperty: someCustomProp", @@ -119,16 +129,17 @@ describe("adl: config file loading", () => { }); it("fails if passing the wrong type", () => { - deepStrictEqual(validator.validateConfig({ emitters: true } as any), [ + deepStrictEqual(validate({ emitters: true } as any), [ { + file, severity: "error", message: "Schema violation: must be object (/emitters)", }, ]); }); - it("succeeeds if config is valid", () => { - deepStrictEqual(validator.validateConfig({ lint: { rules: { foo: "on" } } }), []); + it("succeeds if config is valid", () => { + deepStrictEqual(validate({ lint: { rules: { foo: "on" } } }), []); }); }); }); diff --git a/packages/adl/test/decorators/range-limits.ts b/packages/adl/test/decorators/range-limits.ts index d2253aba4..034fbc824 100644 --- a/packages/adl/test/decorators/range-limits.ts +++ b/packages/adl/test/decorators/range-limits.ts @@ -12,7 +12,7 @@ describe("adl: range limiting decorators", () => { it("applies @minimum and @maximum decorators", async () => { testHost.addAdlFile( - "a.adl", + "main.adl", ` @test model A { @minValue(15) foo: int32; @maxValue(55) boo: float32; } @test model B { @maxValue(20) bar: int64; @minValue(23) car: float64; } diff --git a/packages/adl/test/libraries/test-libraries.ts b/packages/adl/test/libraries/test-libraries.ts index e82b04bd0..9777bacc6 100644 --- a/packages/adl/test/libraries/test-libraries.ts +++ b/packages/adl/test/libraries/test-libraries.ts @@ -12,10 +12,7 @@ describe("adl: libraries", () => { const mainFile = fileURLToPath( new URL(`../../../test/libraries/${lib}/main.adl`, import.meta.url) ); - await createProgram(NodeHost, { - mainFile, - noEmit: true, - }); + await createProgram(NodeHost, mainFile, { noEmit: true }); } catch (e) { console.error(e.diagnostics); throw e; diff --git a/packages/adl/test/test-host.ts b/packages/adl/test/test-host.ts index 290704aeb..4624dd2a0 100644 --- a/packages/adl/test/test-host.ts +++ b/packages/adl/test/test-host.ts @@ -1,5 +1,5 @@ import { readdir, readFile } from "fs/promises"; -import { basename, isAbsolute, join, normalize, relative, resolve, sep } from "path"; +import { basename, extname, isAbsolute, relative, resolve, sep } from "path"; import { fileURLToPath, pathToFileURL } from "url"; import { formatDiagnostic, logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js"; import { CompilerOptions } from "../compiler/options.js"; @@ -22,23 +22,33 @@ export interface TestHost { /** * Virtual filesystem used in the tests. */ - fs: { [name: string]: string }; + fs: Map; +} + +class TestHostError extends Error { + constructor(message: string, public code: "ENOENT" | "ERR_MODULE_NOT_FOUND") { + super(message); + } } export async function createTestHost(): Promise { const testTypes: Record = {}; let program: Program = undefined as any; // in practice it will always be initialized - const virtualFs: { [name: string]: string } = {}; - const jsImports: { [path: string]: Promise } = {}; + const virtualFs = new Map(); + const jsImports = new Map>(); const compilerHost: CompilerHost = { async readFile(path: string) { - return virtualFs[path]; + const contents = virtualFs.get(path); + if (contents === undefined) { + throw new TestHostError(`File ${path} not found.`, "ENOENT"); + } + return contents; }, async readDir(path: string) { const contents = []; - for (const fsPath of Object.keys(virtualFs)) { + for (const fsPath of virtualFs.keys()) { if (isContainedIn(path, fsPath)) { contents.push({ isFile() { @@ -56,7 +66,7 @@ export async function createTestHost(): Promise { }, async writeFile(path: string, content: string) { - virtualFs[path] = content; + virtualFs.set(path, content); }, getLibDirs() { @@ -68,7 +78,11 @@ export async function createTestHost(): Promise { }, getJsImport(path) { - return jsImports[path]; + const module = jsImports.get(path); + if (module === undefined) { + throw new TestHostError(`Module ${path} not found`, "ERR_MODULE_NOT_FOUND"); + } + return module; }, getCwd() { @@ -76,7 +90,7 @@ export async function createTestHost(): Promise { }, async stat(path: string) { - if (virtualFs.hasOwnProperty(path)) { + if (virtualFs.has(path)) { return { isDirectory() { return false; @@ -87,7 +101,7 @@ export async function createTestHost(): Promise { }; } - for (const fsPath of Object.keys(virtualFs)) { + for (const fsPath of virtualFs.keys()) { if (fsPath.startsWith(path) && fsPath !== path) { return { isDirectory() { @@ -100,7 +114,7 @@ export async function createTestHost(): Promise { } } - throw { code: "ENOENT" }; + throw new TestHostError(`File ${path} not found`, "ENOENT"); }, // symlinks not supported in test-host @@ -110,27 +124,33 @@ export async function createTestHost(): Promise { }; // load standard library into the vfs - for (const relDir of ["../../lib", "../../../lib"]) { + for (const [relDir, virtualDir] of [ + ["../../lib", "/.adl/dist/lib"], + ["../../../lib", "/.adl/lib"], + ]) { const dir = resolve(fileURLToPath(import.meta.url), relDir); - const contents = await readdir(dir, { withFileTypes: true }); - for (const entry of contents) { + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const realPath = resolve(dir, entry.name); + const virtualPath = resolve(virtualDir, entry.name); if (entry.isFile()) { - const path = join(dir, entry.name); - const virtualDir = compilerHost.getLibDirs()[0]; - const key = normalize(join(virtualDir, entry.name)); - - if (entry.name.endsWith(".js")) { - jsImports[key] = import(pathToFileURL(path).href); - virtualFs[key] = ""; // don't need contents. - } else { - const contents = await readFile(path, "utf-8"); - virtualFs[key] = contents; + switch (extname(entry.name)) { + case ".adl": + const contents = await readFile(realPath, "utf-8"); + virtualFs.set(virtualPath, contents); + break; + case ".js": + case ".mjs": + jsImports.set(virtualPath, import(pathToFileURL(realPath).href)); + virtualFs.set(virtualPath, ""); // don't need contents. + break; } } } } // add test decorators + addAdlFile("/.adl/test-lib/main.adl", 'import "./test.js";'); addJsFile("/.adl/test-lib/test.js", { test(_: any, target: Type, name?: string) { if (!name) { @@ -161,26 +181,25 @@ export async function createTestHost(): Promise { }; function addAdlFile(path: string, contents: string) { - virtualFs[resolve(compilerHost.getCwd(), path)] = contents; + virtualFs.set(resolve(compilerHost.getCwd(), path), contents); } function addJsFile(path: string, contents: any) { const key = resolve(compilerHost.getCwd(), path); - // don't need file contents; - virtualFs[key] = ""; - jsImports[key] = new Promise((r) => r(contents)); + virtualFs.set(key, ""); // don't need contents + jsImports.set(key, new Promise((r) => r(contents))); } async function addRealAdlFile(path: string, existingPath: string) { - virtualFs[resolve(compilerHost.getCwd(), path)] = await readFile(existingPath, "utf8"); + virtualFs.set(resolve(compilerHost.getCwd(), path), await readFile(existingPath, "utf8")); } async function addRealJsFile(path: string, existingPath: string) { const key = resolve(compilerHost.getCwd(), path); const exports = await import(pathToFileURL(existingPath).href); - virtualFs[key] = ""; - jsImports[key] = exports; + virtualFs.set(key, ""); + jsImports.set(key, exports); } async function compile(main: string, options: CompilerOptions = {}) { @@ -198,18 +217,15 @@ export async function createTestHost(): Promise { } async function compileAndDiagnose( - main: string, + mainFile: string, options: CompilerOptions = {} ): Promise<[Record, readonly Diagnostic[]]> { - // default is noEmit - if (!options.hasOwnProperty("noEmit")) { - options.noEmit = true; + if (options.noEmit === undefined) { + // default for tests is noEmit + options = { ...options, noEmit: true }; } - program = await createProgram(compilerHost, { - mainFile: main, - ...options, - }); + program = await createProgram(compilerHost, mainFile, options); logVerboseTestOutput((log) => logDiagnostics(program.diagnostics, log)); return [testTypes, program.diagnostics]; } From f04292734a1c6daabcf71b9151d9a59b01b5dd81 Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 10 Jun 2021 10:55:46 -0700 Subject: [PATCH 42/47] Handle untitled source files in VS Code (#581) Previously, there was no feedback from language server unless a document had was written somewhere to disk at least once. Feedback happened live, not on save, but if the document was "untitled", then there would be no feedback at all. --- packages/adl-vscode/src/extension.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/adl-vscode/src/extension.ts b/packages/adl-vscode/src/extension.ts index 629c47a98..fbb193c87 100644 --- a/packages/adl-vscode/src/extension.ts +++ b/packages/adl-vscode/src/extension.ts @@ -11,7 +11,10 @@ let client: LanguageClient | undefined; export function activate(context: ExtensionContext) { const exe = resolveADLServer(context); const options: LanguageClientOptions = { - documentSelector: [{ scheme: "file", language: "adl" }], + documentSelector: [ + { scheme: "file", language: "adl" }, + { scheme: "untitled", language: "adl" }, + ], }; const name = "ADL"; From 2ec0c6c45561cae158a384be88f5faf55a9c749b Mon Sep 17 00:00:00 2001 From: Nick Guerrera Date: Thu, 10 Jun 2021 11:16:34 -0700 Subject: [PATCH 43/47] Bump TypeScript version (#582) --- packages/adl-rest/package.json | 2 +- packages/adl-vscode/package.json | 2 +- packages/adl/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/adl-rest/package.json b/packages/adl-rest/package.json index d4bc3d8ac..b07fe5588 100644 --- a/packages/adl-rest/package.json +++ b/packages/adl-rest/package.json @@ -36,6 +36,6 @@ }, "devDependencies": { "@types/node": "~14.0.27", - "typescript": "~4.2.4" + "typescript": "~4.3.2" } } diff --git a/packages/adl-vscode/package.json b/packages/adl-vscode/package.json index 9b66527b3..e47d0dac9 100644 --- a/packages/adl-vscode/package.json +++ b/packages/adl-vscode/package.json @@ -89,7 +89,7 @@ "@types/vscode": "~1.53.0", "mkdirp": "~1.0.4", "rollup": "~2.41.4", - "typescript": "~4.2.4", + "typescript": "~4.3.2", "vsce": "~1.85.1", "vscode-languageclient": "~7.0.0", "watch": "~1.0.2" diff --git a/packages/adl/package.json b/packages/adl/package.json index 26837b296..f07d8a55f 100644 --- a/packages/adl/package.json +++ b/packages/adl/package.json @@ -68,6 +68,6 @@ "mocha": "~8.3.2", "prettier-plugin-organize-imports": "~1.1.1", "source-map-support": "~0.5.19", - "typescript": "~4.2.4" + "typescript": "~4.3.2" } } From 34ea3a669a8c84221646938285492d742893825a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 18 Jun 2021 09:43:28 -0700 Subject: [PATCH 44/47] Feature: ADL Program types walker (#586) --- packages/adl/compiler/checker.ts | 37 ++++++- packages/adl/compiler/program.ts | 4 +- packages/adl/test/checker/global-namespace.ts | 101 ++++++++++++++++++ packages/adl/test/checker/namespaces.ts | 29 +++++ 4 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 packages/adl/test/checker/global-namespace.ts diff --git a/packages/adl/compiler/checker.ts b/packages/adl/compiler/checker.ts index ee03c2343..f239056a1 100644 --- a/packages/adl/compiler/checker.ts +++ b/packages/adl/compiler/checker.ts @@ -46,6 +46,20 @@ import { UnionType, } from "./types.js"; +export interface Checker { + getTypeForNode(node: Node): Type; + checkProgram(program: Program): void; + getGlobalNamespaceType(): NamespaceType; + + getLiteralType(node: StringLiteralNode): StringLiteralType; + getLiteralType(node: NumericLiteralNode): NumericLiteralType; + getLiteralType(node: BooleanLiteralNode): BooleanLiteralType; + getLiteralType(node: LiteralNode): LiteralType; + + getTypeName(type: Type): string; + getNamespaceString(type: NamespaceType | undefined): string; +} + /** * A map keyed by a set of objects. * @@ -92,11 +106,20 @@ interface PendingModelInfo { type: ModelType; } -export function createChecker(program: Program) { +export function createChecker(program: Program): Checker { let templateInstantiation: Type[] = []; let instantiatingTemplate: Node | undefined; let currentSymbolId = 0; const symbolLinks = new Map(); + const root = createType({ + kind: "Namespace", + name: "", + node: program.globalNamespace, + models: new Map(), + operations: new Map(), + namespaces: new Map(), + }); + const errorType: ErrorType = { kind: "Intrinsic", name: "ErrorType" }; // This variable holds on to the model type that is currently @@ -133,7 +156,7 @@ export function createChecker(program: Program) { getLiteralType, getTypeName, getNamespaceString, - checkOperation, + getGlobalNamespaceType, }; function getTypeForNode(node: Node): Type { @@ -197,8 +220,7 @@ export function createChecker(program: Program) { function getNamespaceString(type: NamespaceType | undefined): string { if (!type) return ""; const parent = type.namespace; - - return parent ? `${getNamespaceString(parent)}.${type.name}` : type.name; + return parent && parent.name !== "" ? `${getNamespaceString(parent)}.${type.name}` : type.name; } function getEnumName(e: EnumType): string { @@ -472,7 +494,8 @@ export function createChecker(program: Program) { function getParentNamespaceType( node: ModelStatementNode | NamespaceStatementNode | OperationStatementNode | EnumStatementNode ): NamespaceType | undefined { - if (!node.namespaceSymbol) return undefined; + if (node === root.node) return undefined; + if (!node.namespaceSymbol) return root; const symbolLinks = getSymbolLinks(node.namespaceSymbol); compilerAssert(symbolLinks.type, "Parent namespace isn't typed yet.", node); @@ -494,6 +517,10 @@ export function createChecker(program: Program) { return type; } + function getGlobalNamespaceType() { + return root; + } + function checkTupleExpression(node: TupleExpressionNode): TupleType { return createType({ kind: "Tuple", diff --git a/packages/adl/compiler/program.ts b/packages/adl/compiler/program.ts index 4a0452175..b6e5bd9c5 100644 --- a/packages/adl/compiler/program.ts +++ b/packages/adl/compiler/program.ts @@ -1,7 +1,7 @@ import { dirname, extname, isAbsolute, resolve } from "path"; import resolveModule from "resolve"; import { createBinder, createSymbolTable } from "./binder.js"; -import { createChecker } from "./checker.js"; +import { Checker, createChecker } from "./checker.js"; import { createDiagnostic, createSourceFile, DiagnosticTarget, NoTarget } from "./diagnostics.js"; import { Message } from "./messages.js"; import { CompilerOptions } from "./options.js"; @@ -30,7 +30,7 @@ export interface Program { sourceFiles: ADLScriptNode[]; literalTypes: Map; host: CompilerHost; - checker?: ReturnType; + checker?: Checker; readonly diagnostics: readonly Diagnostic[]; evalAdlScript(adlScript: string, filePath?: string): void; onBuild(cb: (program: Program) => void): Promise | void; diff --git a/packages/adl/test/checker/global-namespace.ts b/packages/adl/test/checker/global-namespace.ts new file mode 100644 index 000000000..f8a0aed08 --- /dev/null +++ b/packages/adl/test/checker/global-namespace.ts @@ -0,0 +1,101 @@ +import { assert } from "console"; +import { createTestHost, TestHost } from "../test-host.js"; + +describe("adl: global namespace", () => { + let testHost: TestHost; + + beforeEach(async () => { + testHost = await createTestHost(); + }); + + describe("it adds top level entities to the global namespace", () => { + it("adds top-level namespaces", async () => { + testHost.addAdlFile("main.adl", `namespace Foo {}`); + + await testHost.compile("./"); + + const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType(); + assert( + globalNamespaceType?.namespaces.get("Foo"), + "Namespace Foo was added to global namespace type" + ); + }); + + it("adds top-level models", async () => { + testHost.addAdlFile("main.adl", `model MyModel {}`); + + await testHost.compile("./"); + + const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType(); + assert( + globalNamespaceType?.models.get("MyModel"), + "model MyModel was added to global namespace type" + ); + }); + + it("adds top-level oeprations", async () => { + testHost.addAdlFile("main.adl", `op myOperation(): string;`); + + await testHost.compile("./"); + + const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType(); + assert( + globalNamespaceType?.operations.get("myOperation"), + "operation myOperation was added to global namespace type" + ); + }); + }); + + describe("it adds top level entities used in other files to the global namespace", () => { + beforeEach(() => { + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + + model Base {} + ` + ); + }); + + it("adds top-level namespaces", async () => { + testHost.addAdlFile("a.adl", `namespace Foo {}`); + + await testHost.compile("./"); + + const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType(); + assert( + globalNamespaceType?.namespaces.get("Foo"), + "Namespace Foo was added to global namespace type" + ); + assert( + globalNamespaceType?.namespaces.get("Base"), + "Should still reference main file top-level entities" + ); + }); + + it("adds top-level models", async () => { + testHost.addAdlFile("a.adl", `model MyModel {}`); + + await testHost.compile("./"); + + const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType(); + assert( + globalNamespaceType?.models.get("MyModel"), + "model MyModel was added to global namespace type" + ); + }); + + it("adds top-level oeprations", async () => { + testHost.addAdlFile("a.adl", `op myOperation(): string;`); + + await testHost.compile("./"); + + const globalNamespaceType = testHost.program.checker?.getGlobalNamespaceType(); + assert( + globalNamespaceType?.operations.get("myOperation"), + "operation myOperation was added to global namespace type" + ); + }); + }); +}); diff --git a/packages/adl/test/checker/namespaces.ts b/packages/adl/test/checker/namespaces.ts index 74df2269f..008430631 100644 --- a/packages/adl/test/checker/namespaces.ts +++ b/packages/adl/test/checker/namespaces.ts @@ -421,3 +421,32 @@ describe("adl: blockless namespaces", () => { strictEqual(Foo.namespaces.size, 1); }); }); + +describe("adl: namespace type name", () => { + let testHost: TestHost; + + beforeEach(async () => { + testHost = await createTestHost(); + }); + + it("prefix with the namespace of the entity", async () => { + testHost.addAdlFile( + "a.adl", + ` + namespace Foo; + + @test() + model Model1 {} + + namespace Other.Bar { + @test() + model Model2 {} + } + ` + ); + + const { Model1, Model2 } = await testHost.compile("/a.adl"); + strictEqual(testHost.program.checker?.getTypeName(Model1), "Foo.Model1"); + strictEqual(testHost.program.checker?.getTypeName(Model2), "Foo.Other.Bar.Model2"); + }); +}); From 630d6c3b0d2ce5afe9eb68e924735219bc92ef76 Mon Sep 17 00:00:00 2001 From: "David Wilson (AZURE SDK)" Date: Fri, 11 Jun 2021 17:04:11 -0700 Subject: [PATCH 45/47] Add mutators library for manipulating evaluated ADL types --- packages/adl/compiler/binder.ts | 15 ++- packages/adl/compiler/checker.ts | 4 + packages/adl/compiler/index.ts | 2 +- packages/adl/compiler/mutators.ts | 161 +++++++++++++++++++++++++++++ packages/adl/compiler/types.ts | 4 +- packages/adl/test/test-host.ts | 7 +- packages/adl/test/test-mutators.ts | 120 +++++++++++++++++++++ 7 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 packages/adl/compiler/mutators.ts create mode 100644 packages/adl/test/test-mutators.ts diff --git a/packages/adl/compiler/binder.ts b/packages/adl/compiler/binder.ts index 17188cf80..b8c710790 100644 --- a/packages/adl/compiler/binder.ts +++ b/packages/adl/compiler/binder.ts @@ -49,15 +49,25 @@ export interface TypeSymbol { export interface Binder { bindSourceFile(program: Program, sourceFile: ADLScriptNode): void; + bindNode(node: Node): void; } export function createSymbolTable(): SymbolTable { return new SymbolTable(); } -export function createBinder(reportDuplicateSymbols: (symbolTable: SymbolTable) => void): Binder { +export interface BinderOptions { + // Configures the initial parent node to use when calling bindNode. This is + // useful for binding ADL fragments outside the context of a full script node. + initialParentNode?: Node; +} + +export function createBinder( + reportDuplicateSymbols: (symbolTable: SymbolTable) => void, + options: BinderOptions = {} +): Binder { let currentFile: ADLScriptNode; - let parentNode: Node; + let parentNode: Node | undefined = options?.initialParentNode; let globalNamespace: NamespaceStatementNode; let fileNamespace: NamespaceStatementNode; let currentNamespace: NamespaceStatementNode; @@ -66,6 +76,7 @@ export function createBinder(reportDuplicateSymbols: (symbolTable: SymbolTable) let scope: ScopeNode; return { bindSourceFile, + bindNode, }; function bindSourceFile(program: Program, sourceFile: ADLScriptNode) { diff --git a/packages/adl/compiler/checker.ts b/packages/adl/compiler/checker.ts index f239056a1..873b858d2 100644 --- a/packages/adl/compiler/checker.ts +++ b/packages/adl/compiler/checker.ts @@ -49,6 +49,8 @@ import { export interface Checker { getTypeForNode(node: Node): Type; checkProgram(program: Program): void; + checkModelProperty(prop: ModelPropertyNode): ModelTypeProperty; + checkUnionExpression(node: UnionExpressionNode): UnionType; getGlobalNamespaceType(): NamespaceType; getLiteralType(node: StringLiteralNode): StringLiteralType; @@ -153,6 +155,8 @@ export function createChecker(program: Program): Checker { return { getTypeForNode, checkProgram, + checkModelProperty, + checkUnionExpression, getLiteralType, getTypeName, getNamespaceString, diff --git a/packages/adl/compiler/index.ts b/packages/adl/compiler/index.ts index 2f92926c1..7454d5880 100644 --- a/packages/adl/compiler/index.ts +++ b/packages/adl/compiler/index.ts @@ -1,8 +1,8 @@ export * from "../lib/decorators.js"; export * from "./diagnostics.js"; +export * from "./mutators.js"; export * from "./parser.js"; export * from "./program.js"; export * from "./types.js"; - import * as formatter from "../formatter/index.js"; export const ADLPrettierPlugin = formatter; diff --git a/packages/adl/compiler/mutators.ts b/packages/adl/compiler/mutators.ts new file mode 100644 index 000000000..7c66243f7 --- /dev/null +++ b/packages/adl/compiler/mutators.ts @@ -0,0 +1,161 @@ +import { createBinder } from "./binder.js"; +import { parse } from "./parser.js"; +import { Program } from "./program.js"; +import { + ModelExpressionNode, + ModelPropertyNode, + ModelStatementNode, + ModelType, + ModelTypeProperty, + Node, + OperationStatementNode, + OperationType, + SyntaxKind, + UnionExpressionNode, +} from "./types.js"; + +function addProperty( + program: Program, + model: ModelType, + modelNode: ModelStatementNode | ModelExpressionNode, + parentNode: Node, + propertyName: string, + propertyTypeName: string, + insertIndex?: number +): ModelTypeProperty { + // Parse a temporary model type to extract its property + const fakeNode = parse(`model Fake { ${propertyName}: ${propertyTypeName}}`); + const firstStatement = fakeNode.statements[0] as ModelStatementNode; + const graftProperty = firstStatement.properties![0] as ModelPropertyNode; + + // Fix up the source location of the nodes to match the model node that + // contains the new property since we can't update the entire file's node + // positions. + graftProperty.pos = modelNode.pos; + graftProperty.end = modelNode.end; + + // Create a binder to wire up the grafted property + const binder = createBinder((dupe) => {}, { + initialParentNode: parentNode, + }); + binder.bindNode(graftProperty); + + // Evaluate the new property with the checker + const newProperty = program.checker!.checkModelProperty(graftProperty); + + // Put the property back into the node + modelNode.properties.splice(insertIndex || modelNode.properties.length, 0, graftProperty); + if (insertIndex !== undefined) { + // Insert the property by adding it in the right order to a new Map + let i = 0; + const newProperties = new Map(); + for (let [name, prop] of model.properties.entries()) { + if (i === insertIndex) { + newProperties.set(newProperty.name, newProperty); + } + newProperties.set(name, prop); + model.properties = newProperties; + i++; + } + } else { + model.properties.set(newProperty.name, newProperty); + } + + return newProperty; +} + +export function addModelProperty( + program: Program, + model: ModelType, + propertyName: string, + propertyTypeName: string +): ModelTypeProperty | undefined { + if (model.node.kind !== SyntaxKind.ModelStatement) { + program.reportDiagnostic( + "Cannot add a model property to anything except a model statement.", + model + ); + + return; + } + + // Create the property and add it to the type + const newProperty = addProperty( + program, + model, + model.node, + model.node, + propertyName, + propertyTypeName + ); + model.properties.set(propertyName, newProperty); + return newProperty; +} + +export interface NewParameterOptions { + // Insert the parameter at the specified index. If `undefined`, add the + // parameter to the end of the parameter list. + insertIndex?: number; +} + +export function addOperationParameter( + program: Program, + operation: OperationType, + parameterName: string, + parameterTypeName: string, + options?: NewParameterOptions +): ModelTypeProperty | undefined { + if (operation.node.kind !== SyntaxKind.OperationStatement) { + program.reportDiagnostic( + "Cannot add a parameter to anything except an operation statement.", + operation + ); + + return; + } + + // Create the property and add it to the type + return addProperty( + program, + operation.parameters, + operation.node.parameters, + operation.node, + parameterName, + parameterTypeName, + options?.insertIndex + ); +} + +export function addOperationResponseType( + program: Program, + operation: OperationType, + responseTypeName: string +) { + if (operation.node.kind !== SyntaxKind.OperationStatement) { + program.reportDiagnostic( + "Cannot add a response to anything except an operation statement.", + operation + ); + + return; + } + + // Parse a temporary operation to extract its response type + const opNode = parse(`op Fake(): string | ${responseTypeName};`); + const graftUnion = (opNode.statements[0] as OperationStatementNode) + .returnType as UnionExpressionNode; + + // Graft the union into the operation + const originalResponse = operation.node.returnType; + graftUnion.options[0] = originalResponse; + operation.node.returnType = graftUnion; + + // Create a binder to wire up the grafted property + const binder = createBinder((dupe) => {}, { + initialParentNode: operation.node, + }); + binder.bindNode(graftUnion); + + // Evaluate the new response type with the checker + operation.returnType = program.checker!.checkUnionExpression(graftUnion); +} diff --git a/packages/adl/compiler/types.ts b/packages/adl/compiler/types.ts index 71f4052c6..79d20cb61 100644 --- a/packages/adl/compiler/types.ts +++ b/packages/adl/compiler/types.ts @@ -75,7 +75,7 @@ export interface OperationType { node: OperationStatementNode; name: string; namespace?: NamespaceType; - parameters?: ModelType; + parameters: ModelType; returnType: Type; } @@ -342,7 +342,7 @@ export interface OperationStatementNode extends BaseNode, DeclarationNode { export interface ModelStatementNode extends BaseNode, DeclarationNode { kind: SyntaxKind.ModelStatement; id: IdentifierNode; - properties?: (ModelPropertyNode | ModelSpreadPropertyNode)[]; + properties: (ModelPropertyNode | ModelSpreadPropertyNode)[]; heritage: ReferenceExpression[]; templateParameters: TemplateParameterDeclarationNode[]; locals?: SymbolTable; diff --git a/packages/adl/test/test-host.ts b/packages/adl/test/test-host.ts index 4624dd2a0..f4596bc73 100644 --- a/packages/adl/test/test-host.ts +++ b/packages/adl/test/test-host.ts @@ -154,7 +154,12 @@ export async function createTestHost(): Promise { addJsFile("/.adl/test-lib/test.js", { test(_: any, target: Type, name?: string) { if (!name) { - if (target.kind === "Model" || target.kind === "Namespace" || target.kind === "Enum") { + if ( + target.kind === "Model" || + target.kind === "Namespace" || + target.kind === "Enum" || + target.kind === "Operation" + ) { name = target.name; } else { throw new Error("Need to specify a name for test type"); diff --git a/packages/adl/test/test-mutators.ts b/packages/adl/test/test-mutators.ts new file mode 100644 index 000000000..572b32249 --- /dev/null +++ b/packages/adl/test/test-mutators.ts @@ -0,0 +1,120 @@ +import { strictEqual } from "assert"; +import { + addModelProperty, + addOperationParameter, + addOperationResponseType, +} from "../compiler/mutators.js"; +import { Program } from "../compiler/program.js"; +import { ModelType, OperationType, UnionType } from "../compiler/types.js"; +import { createTestHost, TestHost } from "./test-host.js"; + +describe("adl: mutators", () => { + let testHost: TestHost; + + beforeEach(async () => { + testHost = await createTestHost(); + }); + + function addBarProperty(program: Program, model: ModelType) { + addModelProperty(program, model, "bar", "string"); + } + + it("addModelProperty adds a property to a model type", async () => { + testHost.addJsFile("a.js", { addBarProperty }); + testHost.addAdlFile( + "main.adl", + ` + import "./a.js"; + + @test + @addBarProperty + model A { foo: int32; } + ` + ); + + const { A } = (await testHost.compile("./")) as { A: ModelType }; + + strictEqual(A.properties.size, 2); + strictEqual(A.properties.get("bar")!.name, "bar"); + strictEqual((A.properties.get("bar")!.type as ModelType).name, "string"); + }); + + function addParameters(program: Program, operation: OperationType) { + addOperationParameter(program, operation, "omega", "string"); + addOperationParameter(program, operation, "alpha", "int64", { insertIndex: 0 }); + addOperationParameter(program, operation, "beta", "B.Excellent", { insertIndex: 1 }); + } + + it("addOperationParameter inserts operation parameters", async () => { + testHost.addJsFile("a.js", { addParameters }); + testHost.addAdlFile( + "main.adl", + ` + import "./a.adl"; + import "./b.adl"; + ` + ); + testHost.addAdlFile( + "a.adl", + ` + import "./a.js"; + import "./b.adl"; + + @test + @addParameters + op TestOp(foo: int32): string; + ` + ); + testHost.addAdlFile( + "b.adl", + ` + namespace B; + model Excellent {} + ` + ); + + const { TestOp } = (await testHost.compile("./")) as { TestOp: OperationType }; + + const params = Array.from(TestOp.parameters.properties.entries()); + strictEqual(TestOp.parameters.properties.size, 4); + strictEqual(params[0][0], "alpha"); + strictEqual((params[0][1].type as ModelType).name, "int64"); + strictEqual(params[1][0], "beta"); + strictEqual((params[1][1].type as ModelType).name, "Excellent"); + strictEqual(params[2][0], "foo"); + strictEqual((params[2][1].type as ModelType).name, "int32"); + strictEqual(params[3][0], "omega"); + strictEqual((params[3][1].type as ModelType).name, "string"); + }); + + function addResponseTypes(program: Program, operation: OperationType) { + addOperationResponseType(program, operation, "int64"); + addOperationResponseType(program, operation, "A.Response"); + } + + it("addModelProperty adds a property to a model type", async () => { + testHost.addJsFile("a.js", { addResponseTypes }); + testHost.addAdlFile( + "main.adl", + ` + import "./a.js"; + + @test + @addResponseTypes + op TestOp(foo: int32): string; + + namespace A { + model Response {} + } + ` + ); + + const { TestOp } = (await testHost.compile("./")) as { TestOp: OperationType }; + + const unionType = TestOp.returnType as UnionType; + strictEqual(unionType.options.length, 3); + strictEqual((unionType.options[0] as ModelType).name, "string"); + strictEqual((unionType.options[1] as ModelType).name, "int64"); + strictEqual((unionType.options[2] as ModelType).name, "Response"); + }); +}); From f093f5c4f80def5a9e54cd275fa0e0309aed91d4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 18 Jun 2021 01:46:56 -0700 Subject: [PATCH 46/47] Pass along diagnostics raised by parsing mutation grafts --- packages/adl/compiler/mutators.ts | 33 +++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/adl/compiler/mutators.ts b/packages/adl/compiler/mutators.ts index 7c66243f7..58948de2c 100644 --- a/packages/adl/compiler/mutators.ts +++ b/packages/adl/compiler/mutators.ts @@ -22,9 +22,19 @@ function addProperty( propertyName: string, propertyTypeName: string, insertIndex?: number -): ModelTypeProperty { +): ModelTypeProperty | undefined { // Parse a temporary model type to extract its property const fakeNode = parse(`model Fake { ${propertyName}: ${propertyTypeName}}`); + if (fakeNode.parseDiagnostics.length > 0) { + program.reportDiagnostic( + `Could not add property/parameter "${propertyName}" of type "${propertyTypeName}"`, + model + ); + program.reportDiagnostics(fakeNode.parseDiagnostics); + + return undefined; + } + const firstStatement = fakeNode.statements[0] as ModelStatementNode; const graftProperty = firstStatement.properties![0] as ModelPropertyNode; @@ -88,8 +98,13 @@ export function addModelProperty( propertyName, propertyTypeName ); - model.properties.set(propertyName, newProperty); - return newProperty; + + if (newProperty) { + model.properties.set(propertyName, newProperty); + return newProperty; + } + + return undefined; } export interface NewParameterOptions { @@ -130,7 +145,7 @@ export function addOperationResponseType( program: Program, operation: OperationType, responseTypeName: string -) { +): any { if (operation.node.kind !== SyntaxKind.OperationStatement) { program.reportDiagnostic( "Cannot add a response to anything except an operation statement.", @@ -142,6 +157,16 @@ export function addOperationResponseType( // Parse a temporary operation to extract its response type const opNode = parse(`op Fake(): string | ${responseTypeName};`); + if (opNode.parseDiagnostics.length > 0) { + program.reportDiagnostic( + `Could not add response type "${responseTypeName}" to operation ${operation.name}"`, + operation + ); + program.reportDiagnostics(opNode.parseDiagnostics); + + return undefined; + } + const graftUnion = (opNode.statements[0] as OperationStatementNode) .returnType as UnionExpressionNode; From f2da3d1f5a4c6f26980a9a4fe19f3971e34260f9 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 21 Jun 2021 08:05:35 +0300 Subject: [PATCH 47/47] Binder now uses reportDuplicateSymbols from Program --- packages/adl/compiler/binder.ts | 11 ++++------- packages/adl/compiler/mutators.ts | 4 ++-- packages/adl/compiler/program.ts | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/adl/compiler/binder.ts b/packages/adl/compiler/binder.ts index b8c710790..38253cda2 100644 --- a/packages/adl/compiler/binder.ts +++ b/packages/adl/compiler/binder.ts @@ -48,7 +48,7 @@ export interface TypeSymbol { } export interface Binder { - bindSourceFile(program: Program, sourceFile: ADLScriptNode): void; + bindSourceFile(sourceFile: ADLScriptNode): void; bindNode(node: Node): void; } @@ -62,10 +62,7 @@ export interface BinderOptions { initialParentNode?: Node; } -export function createBinder( - reportDuplicateSymbols: (symbolTable: SymbolTable) => void, - options: BinderOptions = {} -): Binder { +export function createBinder(program: Program, options: BinderOptions = {}): Binder { let currentFile: ADLScriptNode; let parentNode: Node | undefined = options?.initialParentNode; let globalNamespace: NamespaceStatementNode; @@ -79,7 +76,7 @@ export function createBinder( bindNode, }; - function bindSourceFile(program: Program, sourceFile: ADLScriptNode) { + function bindSourceFile(sourceFile: ADLScriptNode) { globalNamespace = program.globalNamespace; fileNamespace = globalNamespace; currentFile = sourceFile; @@ -132,7 +129,7 @@ export function createBinder( visitChildren(node, bindNode); if (node.kind !== SyntaxKind.NamespaceStatement) { - reportDuplicateSymbols(node.locals!); + program.reportDuplicateSymbols(node.locals!); } scope = prevScope; diff --git a/packages/adl/compiler/mutators.ts b/packages/adl/compiler/mutators.ts index 58948de2c..11073742c 100644 --- a/packages/adl/compiler/mutators.ts +++ b/packages/adl/compiler/mutators.ts @@ -45,7 +45,7 @@ function addProperty( graftProperty.end = modelNode.end; // Create a binder to wire up the grafted property - const binder = createBinder((dupe) => {}, { + const binder = createBinder(program, { initialParentNode: parentNode, }); binder.bindNode(graftProperty); @@ -176,7 +176,7 @@ export function addOperationResponseType( operation.node.returnType = graftUnion; // Create a binder to wire up the grafted property - const binder = createBinder((dupe) => {}, { + const binder = createBinder(program, { initialParentNode: operation.node, }); binder.bindNode(graftUnion); diff --git a/packages/adl/compiler/program.ts b/packages/adl/compiler/program.ts index b6e5bd9c5..e01920990 100644 --- a/packages/adl/compiler/program.ts +++ b/packages/adl/compiler/program.ts @@ -90,7 +90,7 @@ export async function createProgram( }; let virtualFileCount = 0; - const binder = createBinder(program.reportDuplicateSymbols); + const binder = createBinder(program); if (!options?.nostdlib) { await loadStandardLibrary(program); @@ -252,7 +252,7 @@ export async function createProgram( program.reportDiagnostics(sourceFile.parseDiagnostics); program.sourceFiles.push(sourceFile); - binder.bindSourceFile(program, sourceFile); + binder.bindSourceFile(sourceFile); await evalImports(sourceFile); }