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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- "@typespec/compiler"
---

Syntax highlighting does not correctly recognize parameter name with hyphen
212 changes: 186 additions & 26 deletions packages/compiler/src/core/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export type DocToken =
| Token.CloseBrace
| Token.Identifier
| Token.Hyphen
| Token.Dot
| Token.DocText
| Token.DocCodeSpan
| Token.DocCodeFenceDelimiter
Expand Down Expand Up @@ -859,7 +860,10 @@ export function createScanner(
case CharCode.Backtick:
return lookAhead(1) === CharCode.Backtick && lookAhead(2) === CharCode.Backtick
? next(Token.DocCodeFenceDelimiter, 3)
: scanDocCodeSpan();
: scanDocMemberAccessOrIdentifier();

case CharCode.Dot:
return next(Token.Dot);

case CharCode.LessThan:
case CharCode.GreaterThan:
Expand All @@ -873,7 +877,7 @@ export function createScanner(
}

if (isAsciiIdentifierStart(ch)) {
return scanIdentifier();
return scanDocIdentifierOrMemberAccess();
}

if (ch <= CharCode.MaxAscii) {
Expand All @@ -882,7 +886,7 @@ export function createScanner(

const cp = input.codePointAt(position)!;
if (isIdentifierStart(cp)) {
return scanNonAsciiIdentifier(cp);
return scanDocNonAsciiIdentifierOrMemberAccess(cp);
}

return scanUnknown(Token.DocText);
Expand All @@ -891,6 +895,174 @@ export function createScanner(
return (token = Token.EndOfFile);
}

function scanDocIdentifierOrMemberAccess(): Token.Identifier {
if (!scanNormalIdentifierPart()) {
return (token = Token.Identifier);
}

return scanMemberAccessContinuation();
}

function scanDocNonAsciiIdentifierOrMemberAccess(startCodePoint: number): Token.Identifier {
if (!scanNonAsciiIdentifierPart(startCodePoint)) {
return (token = Token.Identifier);
}

return scanMemberAccessContinuation();
}

function scanDocMemberAccessOrIdentifier(): Token.Identifier {
if (!scanSingleBacktickedPart()) {
return unterminated(Token.Identifier);
}

return scanMemberAccessContinuation();
}

function scanMemberAccessContinuation(): Token.Identifier {
while (position < endPosition) {
let tempPos = position;

// Skip whitespace
while (tempPos < endPosition && isWhiteSpaceSingleLine(input.charCodeAt(tempPos))) {
tempPos++;
}

// Check for .
if (tempPos >= endPosition || input.charCodeAt(tempPos) !== CharCode.Dot) {
break;
}
tempPos++; // Skip .

// Skip whitespace after .
while (tempPos < endPosition && isWhiteSpaceSingleLine(input.charCodeAt(tempPos))) {
tempPos++;
}

// Check what comes after .
if (tempPos >= endPosition) {
break;
}

const nextChar = input.charCodeAt(tempPos);
position = tempPos;

if (nextChar === CharCode.Backtick) {
// backticked identifier
if (!scanSingleBacktickedPart()) {
return unterminated(Token.Identifier);
}
} else if (isAsciiIdentifierStart(nextChar)) {
// Ordinary ASCII identifier
if (!scanNormalIdentifierPart()) {
break;
}
} else if (nextChar > CharCode.MaxAscii) {
// Non-ASCII identifier
const cp = input.codePointAt(position)!;
if (isIdentifierStart(cp)) {
if (!scanNonAsciiIdentifierPart(cp)) {
break;
}
} else {
break;
}
} else {
// Not a valid identifier, stop
break;
}
}

return (token = Token.Identifier);
}

function scanSingleBacktickedPart(): boolean {
if (eof() || input.charCodeAt(position) !== CharCode.Backtick) {
return false;
}

position++; // Consume `
tokenFlags |= TokenFlags.Backticked;

while (!eof()) {
const ch = input.charCodeAt(position);
switch (ch) {
case CharCode.Backslash:
position++;
tokenFlags |= TokenFlags.Escaped;
if (!eof()) position++; // Consume escaped characters
continue;
case CharCode.Backtick:
position++; // Consume ending `
return true;
case CharCode.CarriageReturn:
case CharCode.LineFeed:
return false; // Unterminated
default:
if (ch > CharCode.MaxAscii) {
tokenFlags |= TokenFlags.NonAscii;
}
position++;
}
}

return false; // Unterminated
}

function scanNormalIdentifierPart(): boolean {
if (eof()) {
return false;
}

const ch = input.charCodeAt(position);
if (!isAsciiIdentifierStart(ch)) {
return false;
}

// Scan for common identifiers
do {
position++;
if (eof()) {
return true;
}
} while (isAsciiIdentifierContinue(input.charCodeAt(position)));

// Check if there are non-ascii characters
if (!eof()) {
const nextChar = input.charCodeAt(position);
if (nextChar > CharCode.MaxAscii) {
let cp = input.codePointAt(position)!;
if (isNonAsciiIdentifierCharacter(cp)) {
// Contains non-ASCII identifier characters, continue scanning
tokenFlags |= TokenFlags.NonAscii;
do {
position += utf16CodeUnits(cp);
if (eof()) break;
cp = input.codePointAt(position)!;
} while (isIdentifierContinue(cp));
}
}
}

return true;
}

function scanNonAsciiIdentifierPart(startCodePoint: number): boolean {
if (eof()) {
return false;
}

tokenFlags |= TokenFlags.NonAscii;
let cp = startCodePoint;
do {
position += utf16CodeUnits(cp);
if (eof()) break;
cp = input.codePointAt(position)!;
} while (isIdentifierContinue(cp));

return true;
}

function reScanStringTemplate(lastTokenFlags: TokenFlags): StringTemplateToken {
position = tokenPosition;
tokenFlags = TokenFlags.None;
Expand Down Expand Up @@ -1061,24 +1233,6 @@ export function createScanner(
return terminated ? token : unterminated(token);
}

function scanDocCodeSpan(): Token.DocCodeSpan {
position++; // consume '`'

loop: for (; !eof(); position++) {
const ch = input.charCodeAt(position);
switch (ch) {
case CharCode.Backtick:
position++;
return (token = Token.DocCodeSpan);
case CharCode.CarriageReturn:
case CharCode.LineFeed:
break loop;
}
}

return unterminated(Token.DocCodeSpan);
}

function scanString(tokenFlags: TokenFlags): Token.StringLiteral | Token.StringTemplateHead {
if (tokenFlags & TokenFlags.TripleQuoted) {
position += 3; // consume '"""'
Expand Down Expand Up @@ -1205,11 +1359,17 @@ export function createScanner(
}

function getIdentifierTokenValue(): string {
const start = tokenFlags & TokenFlags.Backticked ? tokenPosition + 1 : tokenPosition;
const end =
tokenFlags & TokenFlags.Backticked && !(tokenFlags & TokenFlags.Unterminated)
? position - 1
: position;
// Check if it is a pure backticked identifier (the entire token is surrounded by a single backtick pair)
const isPureBackticked =
tokenFlags & TokenFlags.Backticked &&
!(tokenFlags & TokenFlags.Unterminated) &&
input.charCodeAt(tokenPosition) === CharCode.Backtick &&
input.charCodeAt(position - 1) === CharCode.Backtick &&
// Make sure there are no other backticks in the middle (excluding mixed member access)
input.substring(tokenPosition + 1, position - 1).indexOf("`") === -1;

const start = isPureBackticked ? tokenPosition + 1 : tokenPosition;
const end = isPureBackticked ? position - 1 : position;

const text =
tokenFlags & TokenFlags.Escaped ? unescapeString(start, end) : input.substring(start, end);
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/server/type-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function getSymbolDetails(
}
lines.push(
//prettier-ignore
`_@${tag.tagName.sv}_${"paramName" in tag ? ` \`${tag.paramName.sv}\`` : ""} —\n${getDocContent(tag.content)}`,
`_@${tag.tagName.sv}_${"paramName" in tag ? ` \`${tag.paramName.sv}\`` : ("propName" in tag ? ` \`${tag.propName.sv}\`` : "")} —\n${getDocContent(tag.content)}`,
);
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler/test/scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ describe("compiler: scanner", () => {

[Token.Identifier, "`1!=2`", { pos: 28, value: "1!=2", line: 2, character: 8 }],
[Token.Whitespace, " ", { pos: 34, value: " ", line: 2, character: 14 }],
[Token.Identifier, "`x\\`x`", { pos: 35, value: "x`x", line: 2, character: 15 }],
[Token.Identifier, "`x\\`x`", { pos: 35, value: "`x`x`", line: 2, character: 15 }],
[Token.Whitespace, " ", { pos: 41, value: " ", line: 2, character: 21 }],

[Token.Identifier, "`\\\\x`", { pos: 42, value: "\\x", line: 2, character: 22 }],
Expand All @@ -349,7 +349,7 @@ describe("compiler: scanner", () => {

[Token.Identifier, "`import`", { pos: 55, value: "import", line: 2, character: 35 }],
[Token.Whitespace, " ", { pos: 63, value: " ", line: 2, character: 43 }],
[Token.Identifier, "`a\\n\\t\\`b`", { pos: 64, value: "a\n\t`b", line: 2, character: 44 }],
[Token.Identifier, "`a\\n\\t\\`b`", { pos: 64, value: "`a\n\t`b`", line: 2, character: 44 }],
]);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/protobuf/generated-defs/TypeSpec.Protobuf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type MessageDecorator = (context: DecoratorContext, target: Type) => void
* - not fall within the implementation reserved range of 19000 to 19999, inclusive.
* - not fall within any range that was [marked reserved](#
*
* @TypeSpec .Protobuf.reserve).
* @TypeSpec.Protobuf.reserve ).
*
* #### API Compatibility Note
*
Expand Down
Loading