Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/matej-vscode-diagnostics-comment-awareness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
Comment thread
theoephraim marked this conversation as resolved.
"env-spec-language": patch
---

Fix VS Code diagnostics and completions so decorator parsing ignores prose mentions and post-comments while still matching parser behavior for leading `@word` comment lines.
47 changes: 44 additions & 3 deletions packages/vscode-plugin/src/completion-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,38 @@ const INCOMPATIBLE_DECORATORS = new Map<string, Set<string>>([
['public', new Set(['sensitive'])],
]);

function stripInlineComment(value: string) {
let quote: '"' | '\'' | '' = '';

for (let i = 0; i < value.length; i += 1) {
const char = value[i];
if (quote) {
if (char === quote) quote = '';
continue;
}

if (char === '"' || char === '\'') {
quote = char;
continue;
}

if (char === '#' && (i === 0 || /\s/.test(value[i - 1]))) {
return value.slice(0, i).trimEnd();
}
}

return value.trim();
}

export function getDecoratorCommentPrefix(lineText: string) {
const match = lineText.match(/^\s*#\s*(@.*)$/);
if (!match) return undefined;

const commentText = match[1];

return stripInlineComment(commentText);
}

function splitArgs(input: string) {
const parts: Array<string> = [];
let current = '';
Expand Down Expand Up @@ -94,7 +126,10 @@ export function getExistingDecoratorNames(
if (CONFIG_ITEM_PATTERN.test(text)) break;
if (!text.startsWith('#')) continue;

for (const match of text.matchAll(DECORATOR_PATTERN)) {
const decoratorCommentPrefix = getDecoratorCommentPrefix(text);
if (!decoratorCommentPrefix) continue;

for (const match of decoratorCommentPrefix.matchAll(DECORATOR_PATTERN)) {
names.add(match[1]);
}
}
Expand All @@ -103,7 +138,10 @@ export function getExistingDecoratorNames(
const text = document.lineAt(line).text.trim();
if (!text.startsWith('#')) break;

for (const match of text.matchAll(DECORATOR_PATTERN)) {
const decoratorCommentPrefix = getDecoratorCommentPrefix(text);
if (!decoratorCommentPrefix) continue;

for (const match of decoratorCommentPrefix.matchAll(DECORATOR_PATTERN)) {
names.add(match[1]);
}
}
Expand Down Expand Up @@ -139,7 +177,10 @@ export function getEnumValuesFromPrecedingComments(document: LineDocument, lineN
const text = document.lineAt(line).text.trim();
if (!text.startsWith('#')) break;

const match = text.match(/@type=enum\((.*)\)/);
const decoratorCommentPrefix = getDecoratorCommentPrefix(text);
if (!decoratorCommentPrefix) continue;

const match = decoratorCommentPrefix.match(/@type=enum\((.*)\)/);
if (match) return splitEnumArgs(match[1]);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/vscode-plugin/src/completion-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {

import {
filterAvailableDecorators,
getDecoratorCommentPrefix,
getEnumValuesFromPrecedingComments,
getExistingDecoratorNames,
getTypeOptionDataType,
Expand Down Expand Up @@ -264,7 +265,7 @@ export function addCompletionProvider(context: ExtensionContext) {
provideCompletionItems(document, position) {
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
const commentStart = linePrefix.indexOf('#');
const commentPrefix = commentStart >= 0 ? linePrefix.slice(commentStart + 1) : '';
const commentPrefix = commentStart >= 0 ? getDecoratorCommentPrefix(linePrefix) : undefined;

const referenceContext = matchReference(linePrefix, position);
if (referenceContext) {
Expand All @@ -276,7 +277,7 @@ export function addCompletionProvider(context: ExtensionContext) {
return createEnumValueItems(enumValueContext);
}

if (commentStart >= 0) {
if (commentPrefix) {
const existingDecoratorNames = getExistingDecoratorNames(document, position.line, commentPrefix);

const typeOptionContext = matchTypeOption(commentPrefix, position);
Expand Down
21 changes: 18 additions & 3 deletions packages/vscode-plugin/src/diagnostics-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,23 @@ export function getPrecedingCommentBlock(document: LineDocument, lineNumber: num
return lines;
}

function getDecoratorCommentText(lineText: string) {
const match = lineText.match(/^\s*#\s*(@.*)$/);
if (!match) return undefined;

const commentText = match[1];

return stripInlineComment(commentText);
}

export function getTypeInfoFromPrecedingComments(document: LineDocument, lineNumber: number) {
const commentBlock = getPrecedingCommentBlock(document, lineNumber);

for (let index = commentBlock.length - 1; index >= 0; index -= 1) {
const match = commentBlock[index].match(/@type=([A-Za-z][\w-]*)(?:\((.*)\))?/);
const decoratorComment = getDecoratorCommentText(commentBlock[index]);
if (!decoratorComment) continue;

const match = decoratorComment.match(/@type=([A-Za-z][\w-]*)(?:\((.*)\))?/);
if (!match) continue;

if (match[1] === 'enum') {
Expand All @@ -184,10 +196,13 @@ export function getTypeInfoFromPrecedingComments(document: LineDocument, lineNum

export function getDecoratorOccurrences(lineText: string, lineNumber: number) {
const occurrences: Array<DecoratorOccurrence> = [];
const decoratorComment = getDecoratorCommentText(lineText);
if (!decoratorComment) return occurrences;
const decoratorCommentStart = lineText.indexOf(decoratorComment);

for (const match of lineText.matchAll(DECORATOR_PATTERN)) {
for (const match of decoratorComment.matchAll(DECORATOR_PATTERN)) {
const name = match[1];
const start = match.index ?? 0;
const start = decoratorCommentStart + (match.index ?? 0);
const suffix = match[0].slice(name.length + 1);
occurrences.push({
name,
Expand Down
24 changes: 24 additions & 0 deletions packages/vscode-plugin/test/completion-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';

import {
filterAvailableDecorators,
getDecoratorCommentPrefix,
getEnumValuesFromPrecedingComments,
getExistingDecoratorNames,
getTypeOptionDataType,
Expand Down Expand Up @@ -128,6 +129,29 @@ describe('completion-core', () => {
expect(isInHeader(hyphenDocument, 2)).toBe(false);
});

it('ignores decorator-like text in regular comments', () => {
expect(getDecoratorCommentPrefix('# this @required is docs only')).toBeUndefined();
});

it('matches parser behavior for leading @word comments', () => {
expect(getDecoratorCommentPrefix('# @todo: follow up later')).toBe('@todo: follow up later');
expect(getDecoratorCommentPrefix('# @see docs for more info')).toBe('@see docs for more info');
});

it('ignores decorator-like text in post-comments', () => {
expect(getDecoratorCommentPrefix('# @required # @optional')).toBe('@required');
expect(
getExistingDecoratorNames(
createLineDocument([
'# @required # @optional',
'# @',
]),
1,
' @',
),
).toEqual(new Set(['required']));
});

it('filters duplicate and incompatible decorators but keeps repeatable ones', () => {
const available = filterAvailableDecorators(
ITEM_DECORATORS,
Expand Down
55 changes: 55 additions & 0 deletions packages/vscode-plugin/test/diagnostics-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,43 @@ describe('diagnostics-core', () => {
);
});

it('ignores decorator-like text inside regular comments', () => {
const diagnostics = createDecoratorDiagnostics(
getDecoratorOccurrences('# this @required mention is just documentation', 0),
);

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

it('ignores decorator-like text inside post-comments on decorator lines', () => {
const diagnostics = createDecoratorDiagnostics(
getDecoratorOccurrences('# @required # this @optional is commented', 0),
);

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

it('matches parser behavior for leading @word comments', () => {
expect(getDecoratorOccurrences('# @todo: revisit this later', 0)).toEqual([
{
name: 'todo',
line: 0,
start: 2,
end: 7,
isFunctionCall: false,
},
]);
expect(getDecoratorOccurrences('# @see docs for details', 0)).toEqual([
{
name: 'see',
line: 0,
start: 2,
end: 6,
isFunctionCall: false,
},
]);
});

it('reads type info from the comment block above an item', () => {
const document = createLineDocument([
'# @required @type=url(prependHttps=true, allowedDomains="example.com,api.example.com")',
Expand All @@ -56,6 +93,24 @@ describe('diagnostics-core', () => {
});
});

it('ignores type info inside regular comments above an item', () => {
const document = createLineDocument([
'# mention @type=url(prependHttps=true) in docs only',
'API_URL=example.com',
]);

expect(getTypeInfoFromPrecedingComments(document, 1)).toBeUndefined();
});

it('ignores type info inside post-comments on decorator lines', () => {
const document = createLineDocument([
'# @required # @type=url(prependHttps=true)',
'API_URL=example.com',
]);

expect(getTypeInfoFromPrecedingComments(document, 1)).toBeUndefined();
});

it('validates enum values against the decorator list', () => {
const typeInfo = {
name: 'enum',
Expand Down
Loading