Report errors on node id when possible (#2296)

fix #2084

When a diagnostic target a type/node instead of reporting the error on
the whole type spamming the IDE or terminal with a large warning/error
we only highlight that type id instead.

This is the behavior we can see in typescript/eslint. When the
error/warning is meant to target an interface/function, etc. it will
only highlight the name when able.
This commit is contained in:
Timothee Guerin 2023-08-18 12:42:11 -07:00 коммит произвёл GitHub
Родитель b17e4b1eb3
Коммит bb3d2df9b4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 104 добавлений и 20 удалений

Просмотреть файл

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/compiler",
"comment": "Diagnostics reported on nodes with an `id` will see the diagnostic highlight the id instead of the whole node. For example it will show the error on the model name instead of highlighting the entire model.",
"type": "none"
}
],
"packageName": "@typespec/compiler"
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/protobuf",
"comment": "",
"type": "none"
}
],
"packageName": "@typespec/protobuf"
}

Просмотреть файл

@ -106,7 +106,7 @@ export function logDiagnostics(diagnostics: readonly Diagnostic[], logger: LogSi
level: diagnostic.severity, level: diagnostic.severity,
message: diagnostic.message, message: diagnostic.message,
code: diagnostic.code, code: diagnostic.code,
sourceLocation: getSourceLocation(diagnostic.target), sourceLocation: getSourceLocation(diagnostic.target, { locateId: true }),
}); });
} }
} }
@ -117,7 +117,7 @@ export function formatDiagnostic(diagnostic: Diagnostic) {
code: diagnostic.code, code: diagnostic.code,
level: diagnostic.severity, level: diagnostic.severity,
message: diagnostic.message, message: diagnostic.message,
sourceLocation: getSourceLocation(diagnostic.target), sourceLocation: getSourceLocation(diagnostic.target, { locateId: true }),
}, },
{ pretty: false } { pretty: false }
); );
@ -159,13 +159,28 @@ export function createSourceFile(text: string, path: string): SourceFile {
} }
} }
export function getSourceLocation(target: DiagnosticTarget): SourceLocation; export interface SourceLocationOptions {
export function getSourceLocation(target: typeof NoTarget | undefined): undefined; /**
* If trying to resolve the location of a type with an ID, show the location of the ID node instead of the entire type.
* This makes sure that the location range is not too large and hard to read.
*/
locateId?: boolean;
}
export function getSourceLocation( export function getSourceLocation(
target: DiagnosticTarget | typeof NoTarget | undefined target: DiagnosticTarget,
options?: SourceLocationOptions
): SourceLocation;
export function getSourceLocation(
target: typeof NoTarget | undefined,
options?: SourceLocationOptions
): undefined;
export function getSourceLocation(
target: DiagnosticTarget | typeof NoTarget | undefined,
options?: SourceLocationOptions
): SourceLocation | undefined; ): SourceLocation | undefined;
export function getSourceLocation( export function getSourceLocation(
target: DiagnosticTarget | typeof NoTarget | undefined target: DiagnosticTarget | typeof NoTarget | undefined,
options: SourceLocationOptions = {}
): SourceLocation | undefined { ): SourceLocation | undefined {
if (target === NoTarget || target === undefined) { if (target === NoTarget || target === undefined) {
return undefined; return undefined;
@ -185,16 +200,16 @@ export function getSourceLocation(
return createSyntheticSourceLocation(); return createSyntheticSourceLocation();
} }
return getSourceLocationOfNode(target.declarations[0]); return getSourceLocationOfNode(target.declarations[0], options);
} else if (typeof target.kind === "number") { } else if (typeof target.kind === "number") {
// node // node
return getSourceLocationOfNode(target as Node); return getSourceLocationOfNode(target as Node, options);
} else { } else {
// type // type
const targetNode = (target as Type).node; const targetNode = (target as Type).node;
if (targetNode) { if (targetNode) {
return getSourceLocationOfNode(targetNode); return getSourceLocationOfNode(targetNode, options);
} }
return createSyntheticSourceLocation(); return createSyntheticSourceLocation();
@ -210,7 +225,7 @@ function createSyntheticSourceLocation(loc = "<unknown location>") {
}; };
} }
function getSourceLocationOfNode(node: Node): SourceLocation { function getSourceLocationOfNode(node: Node, options: SourceLocationOptions): SourceLocation {
let root = node; let root = node;
while (root.parent !== undefined) { while (root.parent !== undefined) {
@ -225,6 +240,10 @@ function getSourceLocationOfNode(node: Node): SourceLocation {
); );
} }
if (options.locateId && "id" in node && node.id !== undefined) {
node = node.id;
}
return { return {
file: root.file, file: root.file,
pos: node.pos, pos: node.pos,

Просмотреть файл

@ -38,6 +38,6 @@ function processLog(log: LogInfo): ProcessedLog {
level: log.level, level: log.level,
code: log.code, code: log.code,
message: log.message, message: log.message,
sourceLocation: getSourceLocation(log.target), sourceLocation: getSourceLocation(log.target, { locateId: true }),
}; };
} }

Просмотреть файл

@ -577,7 +577,7 @@ export function createServer(host: ServerHost): Server {
for (const each of program.diagnostics) { for (const each of program.diagnostics) {
let document: TextDocument | undefined; let document: TextDocument | undefined;
const location = getSourceLocation(each.target); const location = getSourceLocation(each.target, { locateId: true });
if (location?.file) { if (location?.file) {
document = (location.file as ServerSourceFile).document; document = (location.file as ServerSourceFile).document;
} else { } else {

Просмотреть файл

@ -0,0 +1,45 @@
import { strictEqual } from "assert";
import { SourceLocationOptions, getSourceLocation } from "../../src/index.js";
import { createTestRunner } from "../../src/testing/test-host.js";
import { extractSquiggles } from "../../src/testing/test-server-host.js";
import { BasicTestRunner } from "../../src/testing/types.js";
describe("compiler: diagnostics", () => {
let runner: BasicTestRunner;
beforeEach(async () => {
runner = await createTestRunner();
});
async function expectLocationMatch(code: string, options: SourceLocationOptions = {}) {
const { pos, end, source } = extractSquiggles(code);
const { target } = await runner.compile(source);
const location = getSourceLocation(target, options);
strictEqual(location.pos, pos);
strictEqual(location.end, end);
}
describe("getSourceLocation", () => {
it("report whole model by default", () =>
expectLocationMatch(`
~~~@doc("This is documentation")
@test("target")
model Foo {
name: string;
}~~~
`));
it("report report only model id if `locateId: true`", () =>
expectLocationMatch(
`
@doc("This is documentation")
@test("target")
model ~~~Foo~~~ {
name: string;
}
`,
{ locateId: true }
));
});
});

Просмотреть файл

@ -1,4 +1,4 @@
/test/main.tsp:18:1 - error @typespec/protobuf/unconvertible-enum: enums must explicitly assign exactly one integer to each member to be used in a Protobuf message /test/main.tsp:18:6 - error @typespec/protobuf/unconvertible-enum: enums must explicitly assign exactly one integer to each member to be used in a Protobuf message
/test/main.tsp:19:3 - error @typespec/protobuf/unconvertible-enum: the first variant of an enum must be set to zero to be used in a Protobuf message /test/main.tsp:19:3 - error @typespec/protobuf/unconvertible-enum: the first variant of an enum must be set to zero to be used in a Protobuf message
/test/main.tsp:23:1 - error @typespec/protobuf/unconvertible-enum: enums must explicitly assign exactly one integer to each member to be used in a Protobuf message /test/main.tsp:23:6 - error @typespec/protobuf/unconvertible-enum: enums must explicitly assign exactly one integer to each member to be used in a Protobuf message
/test/main.tsp:24:3 - error @typespec/protobuf/unconvertible-enum: the first variant of an enum must be set to zero to be used in a Protobuf message /test/main.tsp:24:3 - error @typespec/protobuf/unconvertible-enum: the first variant of an enum must be set to zero to be used in a Protobuf message

Просмотреть файл

@ -1,6 +1,6 @@
/test/main.tsp:12:5 - error @typespec/protobuf/field-index: field index 0 is invalid (must be an integer greater than zero) /test/main.tsp:12:15 - error @typespec/protobuf/field-index: field index 0 is invalid (must be an integer greater than zero)
/test/main.tsp:13:5 - error @typespec/protobuf/field-index: field index 536870912 is out of bounds (must be less than 536870912) /test/main.tsp:13:23 - error @typespec/protobuf/field-index: field index 536870912 is out of bounds (must be less than 536870912)
/test/main.tsp:14:5 - error @typespec/protobuf/field-index: field index 19000 falls within the implementation-reserved range of 19000-19999 inclusive /test/main.tsp:14:19 - error @typespec/protobuf/field-index: field index 19000 falls within the implementation-reserved range of 19000-19999 inclusive
/test/main.tsp:15:5 - error @typespec/protobuf/field-index: field index 19123 falls within the implementation-reserved range of 19000-19999 inclusive /test/main.tsp:15:19 - error @typespec/protobuf/field-index: field index 19123 falls within the implementation-reserved range of 19000-19999 inclusive
/test/main.tsp:16:5 - error @typespec/protobuf/field-index: field index 19999 falls within the implementation-reserved range of 19000-19999 inclusive /test/main.tsp:16:19 - error @typespec/protobuf/field-index: field index 19999 falls within the implementation-reserved range of 19000-19999 inclusive
/test/main.tsp:21:1 - error decorator-wrong-target: Cannot apply @message decorator to Test.Test since it is not assignable to {} /test/main.tsp:21:1 - error decorator-wrong-target: Cannot apply @message decorator to Test.Test since it is not assignable to {}

Просмотреть файл

@ -1 +1 @@
/test/main.tsp:14:3 - error @typespec/protobuf/unsupported-field-type: a message field's type may not be a union /test/main.tsp:14:13 - error @typespec/protobuf/unsupported-field-type: a message field's type may not be a union