Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report diagnostics for unused "using" so that they will be shown properly in ide #5453

Merged
merged 31 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
64be743
add code for unused using
RodgeFu Dec 13, 2024
a1862d8
fix some tests
RodgeFu Dec 13, 2024
1c33f00
Merge remote-tracking branch 'upstream/main' into unused-using
RodgeFu Dec 26, 2024
bf0c11b
add changelog
RodgeFu Dec 26, 2024
7a78fdd
Merge branch 'main' into unused-using
RodgeFu Dec 26, 2024
a01cd7f
report hint as info to server log
RodgeFu Dec 26, 2024
d50e959
fix tests
RodgeFu Dec 27, 2024
0478187
fix test
RodgeFu Dec 27, 2024
52298a9
fix test and add one more test case
RodgeFu Dec 27, 2024
46ccfca
fix test
RodgeFu Dec 27, 2024
50e995b
fix e2e test
RodgeFu Dec 27, 2024
42a3db2
add changelog
RodgeFu Dec 27, 2024
73f2bc7
Merge branch 'main' into unused-using
RodgeFu Jan 13, 2025
299c118
Merge remote-tracking branch 'upstream/main' into unused-using
RodgeFu Jan 15, 2025
6617403
refine the message for unused using
RodgeFu Jan 15, 2025
c3b3e9b
Merge remote-tracking branch 'upstream/main' into unused-using
RodgeFu Jan 16, 2025
7d2d58d
update test
RodgeFu Jan 17, 2025
2ce546c
Merge remote-tracking branch 'upstream/main' into unused-using
RodgeFu Jan 17, 2025
a68e286
fix test
RodgeFu Jan 17, 2025
a5a4e38
Merge remote-tracking branch 'upstream/main' into unused-using
RodgeFu Jan 29, 2025
4060338
using linter to report unused-using
RodgeFu Feb 13, 2025
4df823c
update test
RodgeFu Feb 13, 2025
38c769a
Merge remote-tracking branch 'upstream/main' into unused-using
RodgeFu Feb 13, 2025
3eaf6ab
update tests
RodgeFu Feb 13, 2025
55db27b
update tests for unused using
RodgeFu Feb 13, 2025
35c674b
remove unnecessary changelot
RodgeFu Feb 13, 2025
98c5b81
add more test for unused using's logic in lsp
RodgeFu Feb 13, 2025
0a22aa3
refine some code and add more test
RodgeFu Feb 13, 2025
a95875e
update linter to expose registerLinterLibrary method
RodgeFu Feb 15, 2025
872495d
update test per feedback
RodgeFu Feb 15, 2025
59e6b2a
Merge branch 'main' into unused-using
RodgeFu Feb 16, 2025
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
7 changes: 7 additions & 0 deletions .chronus/changes/unused-using-2024-11-26-19-12-40.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Support diagnostics for unused using statement
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineCodeFix, getSourceLocation } from "../diagnostics.js";
import { type ImportStatementNode, type UsingStatementNode } from "../types.js";

/**
* Quick fix that remove unused code.
*/
export function removeUnusedCodeCodeFix(node: ImportStatementNode | UsingStatementNode) {
return defineCodeFix({
id: "remove-unused-code",
label: `Remove unused code`,
fix: (context) => {
const location = getSourceLocation(node);
return context.replaceText(location, "");
},
});
}
74 changes: 68 additions & 6 deletions packages/compiler/src/core/linter.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import { removeUnusedCodeCodeFix } from "./compiler-code-fixes/remove-unused-code.codefix.js";
import { DiagnosticCollector, compilerAssert, createDiagnosticCollector } from "./diagnostics.js";
import { getLocationContext } from "./helpers/location-context.js";
import { createLinterRule, defineLinter, paramMessage } from "./library.js";
import { createDiagnostic } from "./messages.js";
import { NameResolver } from "./name-resolver.js";
import type { Program } from "./program.js";
import { EventEmitter, mapEventEmitterToNodeListener, navigateProgram } from "./semantic-walker.js";
import {
Diagnostic,
DiagnosticMessages,
LibraryInstance,
IdentifierNode,
LinterDefinition,
LinterResolvedDefinition,
LinterRule,
LinterRuleContext,
LinterRuleDiagnosticReport,
LinterRuleSet,
MemberExpressionNode,
NoTarget,
RuleRef,
SemanticNodeListener,
SyntaxKind,
} from "./types.js";

type LinterLibraryInstance = { linter: LinterResolvedDefinition };

export interface Linter {
extendRuleSet(ruleSet: LinterRuleSet): Promise<readonly Diagnostic[]>;
registerLinterLibrary(name: string, lib?: LinterLibraryInstance): void;
lint(): readonly Diagnostic[];
}

Expand Down Expand Up @@ -53,16 +61,17 @@ export function resolveLinterDefinition(

export function createLinter(
program: Program,
loadLibrary: (name: string) => Promise<LibraryInstance | undefined>,
loadLibrary: (name: string) => Promise<LinterLibraryInstance | undefined>,
): Linter {
const tracer = program.tracer.sub("linter");

const ruleMap = new Map<string, LinterRule<string, any>>();
const enabledRules = new Map<string, LinterRule<string, any>>();
const linterLibraries = new Map<string, LibraryInstance | undefined>();
const linterLibraries = new Map<string, LinterLibraryInstance | undefined>();

return {
extendRuleSet,
registerLinterLibrary,
lint,
};

Expand Down Expand Up @@ -158,18 +167,21 @@ export function createLinter(
return diagnostics.diagnostics;
}

async function resolveLibrary(name: string): Promise<LibraryInstance | undefined> {
async function resolveLibrary(name: string): Promise<LinterLibraryInstance | undefined> {
const loadedLibrary = linterLibraries.get(name);
if (loadedLibrary === undefined) {
return registerLinterLibrary(name);
}
return loadedLibrary;
}

async function registerLinterLibrary(name: string): Promise<LibraryInstance | undefined> {
async function registerLinterLibrary(
name: string,
lib?: LinterLibraryInstance,
): Promise<LinterLibraryInstance | undefined> {
tracer.trace("register-library", name);

const library = await loadLibrary(name);
const library = lib ?? (await loadLibrary(name));
const linter = library?.linter;
if (linter?.rules) {
for (const rule of linter.rules) {
Expand Down Expand Up @@ -253,3 +265,53 @@ export function createLinterRuleContext<N extends string, DM extends DiagnosticM
}
}
}

export const builtInLinterLibraryName = `@typespec/compiler`;
export const builtInLinterRule_UnusedUsing = `unused-using`;
export function createBuiltInLinterLibrary(nameResolver: NameResolver): LinterLibraryInstance {
const builtInLinter: LinterResolvedDefinition = resolveLinterDefinition(
builtInLinterLibraryName,
createBuiltInLinter(nameResolver),
);
return { linter: builtInLinter };
}
function createBuiltInLinter(nameResolver: NameResolver): LinterDefinition {
const unusedUsingLinterRule = createUnusedUsingLinterRule();

return defineLinter({
rules: [unusedUsingLinterRule],
});

function createUnusedUsingLinterRule() {
return createLinterRule({
name: builtInLinterRule_UnusedUsing,
severity: "warning",
description: "Linter rules for unused using statement.",
messages: {
default: paramMessage`'using ${"code"}' is declared but never be used.`,
},
create(context) {
return {
root: (_root) => {
const getUsingName = (node: MemberExpressionNode | IdentifierNode): string => {
if (node.kind === SyntaxKind.MemberExpression) {
return `${getUsingName(node.base)}${node.selector}${node.id.sv}`;
} else {
// identifier node
return node.sv;
}
};
nameResolver.getUnusedUsings().forEach((target) => {
context.reportDiagnostic({
messageId: "default",
format: { code: getUsingName(target.name) },
target,
codefixes: [removeUnusedCodeCodeFix(target)],
});
});
},
};
},
});
}
}
49 changes: 48 additions & 1 deletion packages/compiler/src/core/name-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
import { Mutable, mutate } from "../utils/misc.js";
import { createSymbol, createSymbolTable, getSymNode } from "./binder.js";
import { compilerAssert } from "./diagnostics.js";
import { visitChildren } from "./parser.js";
import { getFirstAncestor, visitChildren } from "./parser.js";
import { Program } from "./program.js";
import {
AliasStatementNode,
Expand Down Expand Up @@ -95,6 +95,7 @@ import {
TypeReferenceNode,
TypeSpecScriptNode,
UnionStatementNode,
UsingStatementNode,
} from "./types.js";

export interface NameResolver {
Expand Down Expand Up @@ -143,6 +144,9 @@ export interface NameResolver {
node: TypeReferenceNode | IdentifierNode | MemberExpressionNode,
): ResolutionResult;

/** Get the using statement nodes which is not used in resolving yet */
getUnusedUsings(): UsingStatementNode[];

/** Built-in symbols. */
readonly symbols: {
/** Symbol for the global namespace */
Expand Down Expand Up @@ -176,6 +180,11 @@ export function createResolver(program: Program): NameResolver {
mutate(globalNamespaceNode).symbol = globalNamespaceSym;
mutate(globalNamespaceSym.exports).set(globalNamespaceNode.id.sv, globalNamespaceSym);

/**
* Tracking the symbols that are used through using.
*/
const usedUsingSym = new Map<TypeSpecScriptNode, Set<Sym>>();

const metaTypePrototypes = createMetaTypePrototypes();

const nullSym = createSymbol(undefined, "null", SymbolFlags.None);
Expand Down Expand Up @@ -223,8 +232,33 @@ export function createResolver(program: Program): NameResolver {
resolveTypeReference,

getAugmentDecoratorsForSym,
getUnusedUsings,
};

function getUnusedUsings(): UsingStatementNode[] {
const unusedUsings: Set<UsingStatementNode> = new Set();
for (const file of program.sourceFiles.values()) {
const lc = program.getSourceFileLocationContext(file.file);
if (lc.type === "project") {
const usedSym = usedUsingSym.get(file) ?? new Set<Sym>();
for (const using of file.usings) {
const table = getNodeLinks(using.name).resolvedSymbol;
let used = false;
for (const [_, sym] of table?.exports ?? new Map<string, Sym>()) {
if (usedSym.has(getMergedSymbol(sym))) {
used = true;
break;
}
}
if (used === false) {
unusedUsings.add(using);
}
}
}
}
return [...unusedUsings];
}

function getAugmentDecoratorsForSym(sym: Sym) {
return augmentDecoratorsForSym.get(sym) ?? [];
}
Expand Down Expand Up @@ -961,6 +995,15 @@ export function createResolver(program: Program): NameResolver {
if ("locals" in scope && scope.locals !== undefined) {
binding = tableLookup(scope.locals, node, options.resolveDecorators);
if (binding) {
if (binding.flags & SymbolFlags.Using && binding.symbolSource) {
const fileNode = getFirstAncestor(node, (n) => n.kind === SyntaxKind.TypeSpecScript) as
| TypeSpecScriptNode
| undefined;
if (fileNode) {
usedUsingSym.get(fileNode)?.add(binding.symbolSource) ??
usedUsingSym.set(fileNode, new Set([binding.symbolSource]));
}
}
return resolvedResult(binding);
}
}
Expand Down Expand Up @@ -998,6 +1041,10 @@ export function createResolver(program: Program): NameResolver {
[]),
]);
}
if (usingBinding.flags & SymbolFlags.Using && usingBinding.symbolSource) {
usedUsingSym.get(scope)?.add(usingBinding.symbolSource) ??
usedUsingSym.set(scope, new Set([usingBinding.symbolSource]));
}
return resolvedResult(usingBinding.symbolSource!);
}
}
Expand Down
13 changes: 10 additions & 3 deletions packages/compiler/src/core/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import { compilerAssert } from "./diagnostics.js";
import { resolveTypeSpecEntrypoint } from "./entrypoint-resolution.js";
import { ExternalError } from "./external-error.js";
import { getLibraryUrlsLoaded } from "./library.js";
import { createLinter, resolveLinterDefinition } from "./linter.js";
import {
builtInLinterLibraryName,
createBuiltInLinterLibrary,
createLinter,
resolveLinterDefinition,
} from "./linter.js";
import { createLogger } from "./logger/index.js";
import { createTracer } from "./logger/tracer.js";
import { createDiagnostic } from "./messages.js";
Expand Down Expand Up @@ -230,13 +235,15 @@ export async function compile(
oldProgram = undefined;
setCurrentProgram(program);

const resolver = createResolver(program);
resolver.resolveProgram();

const linter = createLinter(program, (name) => loadLibrary(basedir, name));
linter.registerLinterLibrary(builtInLinterLibraryName, createBuiltInLinterLibrary(resolver));
if (options.linterRuleSet) {
program.reportDiagnostics(await linter.extendRuleSet(options.linterRuleSet));
}

const resolver = createResolver(program);
resolver.resolveProgram();
program.checker = createChecker(program, resolver);
program.checker.checkProgram();

Expand Down
20 changes: 16 additions & 4 deletions packages/compiler/src/server/compile-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
joinPaths,
parse,
} from "../core/index.js";
import { builtInLinterLibraryName, builtInLinterRule_UnusedUsing } from "../core/linter.js";
import { compile as compileProgram } from "../core/program.js";
import { doIO, loadFile, resolveTspMain } from "../utils/misc.js";
import { serverOptions } from "./constants.js";
Expand Down Expand Up @@ -107,6 +108,17 @@ export function createCompileService({
...optionsFromConfig,
...serverOptions,
};
// add linter rule for unused using if user didn't configure it explicitly
const unusedUsingRule = `${builtInLinterLibraryName}/${builtInLinterRule_UnusedUsing}`;
if (
options.linterRuleSet?.enable?.[unusedUsingRule] === undefined &&
options.linterRuleSet?.disable?.[unusedUsingRule] === undefined
) {
options.linterRuleSet ??= {};
options.linterRuleSet.enable ??= {};
options.linterRuleSet.enable[unusedUsingRule] = true;
}

log({ level: "debug", message: `compiler options resolved`, detail: options });

if (!fileService.upToDate(document)) {
Expand Down Expand Up @@ -142,7 +154,7 @@ export function createCompileService({
const script = program.sourceFiles.get(resolvedPath);
compilerAssert(script, "Failed to get script.");

const result: CompileResult = { program, document: doc, script };
const result: CompileResult = { program, document: doc, script, optionsFromConfig };
notify("compileEnd", result);
return result;
} catch (err: any) {
Expand Down Expand Up @@ -180,13 +192,13 @@ export function createCompileService({
}

const cached = await fileSystemCache.get(configPath);
const deepCopy = (obj: any) => JSON.parse(JSON.stringify(obj));
if (cached?.data) {
return cached.data;
return deepCopy(cached.data);
}

const config = await loadTypeSpecConfigFile(compilerHost, configPath);
await fileSystemCache.setData(configPath, config);
return config;
return deepCopy(config);
}

async function getScript(
Expand Down
17 changes: 16 additions & 1 deletion packages/compiler/src/server/serverlib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CompletionList,
CompletionParams,
DefinitionParams,
DiagnosticSeverity,
DiagnosticTag,
DidChangeWatchedFilesParams,
DocumentFormattingParams,
Expand Down Expand Up @@ -57,6 +58,7 @@ import {
ResolveModuleHost,
typespecVersion,
} from "../core/index.js";
import { builtInLinterLibraryName, builtInLinterRule_UnusedUsing } from "../core/linter.js";
import { formatLog } from "../core/logger/index.js";
import { getPositionBeforeTrivia } from "../core/parser-utils.js";
import { getNodeAtPosition, getNodeAtPositionDetail, visitChildren } from "../core/parser.js";
Expand Down Expand Up @@ -467,7 +469,7 @@ export function createServer(host: ServerHost): Server {

compileService.notifyChange(change.document);
}
async function reportDiagnostics({ program, document }: CompileResult) {
async function reportDiagnostics({ program, document, optionsFromConfig }: CompileResult) {
if (isTspConfigFile(document)) return undefined;

currentDiagnosticIndex.clear();
Expand Down Expand Up @@ -495,8 +497,21 @@ export function createServer(host: ServerHost): Server {
href: each.url,
};
}
const unusedUsingRule = `${builtInLinterLibraryName}/${builtInLinterRule_UnusedUsing}`;
if (each.code === "deprecated") {
diagnostic.tags = [DiagnosticTag.Deprecated];
} else if (each.code === unusedUsingRule) {
// Unused or unnecessary code. Diagnostics with this tag are rendered faded out, so no extra work needed from IDE side
// https://vscode-api.js.org/enums/vscode.DiagnosticTag.html#google_vignette
// https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.languageserver.protocol.diagnostictag?view=visualstudiosdk-2022
diagnostic.tags = [DiagnosticTag.Unnecessary];
if (
optionsFromConfig.linterRuleSet?.enable?.[unusedUsingRule] === undefined &&
optionsFromConfig.linterRuleSet?.disable?.[unusedUsingRule] === undefined
) {
// if the unused using is not configured by user explicitly, report it as hint by default
diagnostic.severity = DiagnosticSeverity.Hint;
}
}
diagnostic.data = { id: diagnosticIdCounter++ };
const diagnostics = diagnosticMap.get(diagDocument);
Expand Down
Loading
Loading