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:
Родитель
b17e4b1eb3
Коммит
bb3d2df9b4
|
@ -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
|
||||||
|
|
Загрузка…
Ссылка в новой задаче