diff --git a/.gitignore b/.gitignore index a597b94..b0ef2d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,16 @@ -node_modules/ +# Build output out/ -coverage/ +dist/ + +# Dependencies +node_modules/ + +# Test runner and coverage .vscode-test/ +coverage/ + +# Packaged extension *.vsix + +# macOS +.DS_Store diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..57b8d1c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Pathverse Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index e69de29..d22f9ba 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,28 @@ +# Dart References + +Show reference counts for Dart symbols directly in the editor using CodeLens, with optional warnings for unused functions and methods. + +## Requirements + +- VS Code 1.100.0+ +- A Dart language server (install the official Dart/Flutter VS Code extension) + +## How It Works + +- Uses document symbols to find Dart functions, classes, methods, constructors, variables, and enums. +- For each symbol, queries references and shows a CodeLens label. +- If a function or method has zero references, a warning diagnostic is added. + +## Configuration + +Settings are under `dartReferences`: + +- `dartReferences.enable`: Enable/disable CodeLens and diagnostics (default: `true`). +- `dartReferences.referencesLabel`: Label used for non-zero reference counts (default: `references`). +- `dartReferences.zeroReferencesLabel`: Label used for zero references (default: `No references`). +- `dartReferences.ignoredMethods`: Method names to ignore (default: Flutter lifecycle and common overrides). + +## Notes + +- Reference counts rely on the Dart language server; results may be delayed in large files. +- Ignored methods are matched by name only (not by class or signature). diff --git a/dart-logo.png b/dart-logo.png new file mode 100644 index 0000000..f79b54f Binary files /dev/null and b/dart-logo.png differ diff --git a/package.json b/package.json index 537c091..39afd7c 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "dart-references", - "displayName": "dart-references", + "displayName": "Dart References", "description": "Displays the number of references for Dart and Flutter in VSCode", + "author": "Pathverse Inc.", + "license": "MIT", "version": "0.1.0", + "icon": "dart-logo.png", "publisher": "pathverse", "repository": { "type": "git", @@ -17,7 +20,6 @@ "activationEvents": [ "onLanguage:dart" ], - "icon": "icon.png", "main": "./out/extension.js", "contributes": { "configuration": { @@ -38,6 +40,25 @@ "default": "No references", "description": "Label displayed when there are no references found for a Dart functions, classes, methods, constructors, variables, and enums." }, + "dartReferences.ignoredMethods": { + "type": "array", + "default": [ + "build", + "initState", + "dispose", + "didChangeDependencies", + "didUpdateWidget", + "reassemble", + "deactivate", + "setState", + "createState", + "toString", + "hashCode", + "operator==", + "noSuchMethod" + ], + "description": "List of method names to ignore when displaying reference counts (e.g., Flutter lifecycle methods and common overrides)." + }, "dartReferences.cacheMaxEntries": { "type": "number", "default": 2000, diff --git a/src/core/settings.ts b/src/core/settings.ts index a0cf8eb..447392a 100644 --- a/src/core/settings.ts +++ b/src/core/settings.ts @@ -2,6 +2,8 @@ export interface DartReferencesSettings { enabled: boolean; referencesLabel: string; zeroReferencesLabel: string; + /** Method names that never get a lens or unused diagnostic (e.g. Flutter lifecycle overrides). */ + ignoredMethods: ReadonlySet; /** Maximum cached symbols (LRU-evicted); 0 = unlimited. */ cacheMaxEntries: number; /** Above this many locations only the count is cached; 0 = unlimited. */ @@ -14,6 +16,7 @@ export interface RawSettings { enable?: unknown; referencesLabel?: unknown; zeroReferencesLabel?: unknown; + ignoredMethods?: unknown; cacheMaxEntries?: unknown; maxCachedLocations?: unknown; cacheTtlSeconds?: unknown; @@ -27,6 +30,30 @@ function labelOrDefault(raw: unknown, fallback: string): string { return trimmed.length > 0 ? trimmed : fallback; } +// Mirrors the dartReferences.ignoredMethods default in package.json. +const defaultIgnoredMethods: readonly string[] = [ + 'build', + 'initState', + 'dispose', + 'didChangeDependencies', + 'didUpdateWidget', + 'reassemble', + 'deactivate', + 'setState', + 'createState', + 'toString', + 'hashCode', + 'operator==', + 'noSuchMethod', +]; + +function ignoredMethodsOrDefault(raw: unknown): ReadonlySet { + if (!Array.isArray(raw)) { + return new Set(defaultIgnoredMethods); + } + return new Set(raw.filter((name): name is string => typeof name === 'string')); +} + function limitOrDefault(raw: unknown, fallback: number): number { if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0) { return fallback; @@ -39,6 +66,7 @@ export function normalizeSettings(raw: RawSettings): DartReferencesSettings { enabled: raw.enable !== false, referencesLabel: labelOrDefault(raw.referencesLabel, 'references'), zeroReferencesLabel: labelOrDefault(raw.zeroReferencesLabel, 'No references'), + ignoredMethods: ignoredMethodsOrDefault(raw.ignoredMethods), cacheMaxEntries: limitOrDefault(raw.cacheMaxEntries, 2000), maxCachedLocations: limitOrDefault(raw.maxCachedLocations, 1000), cacheTtlMs: limitOrDefault(raw.cacheTtlSeconds, 60) * 1000, diff --git a/src/extension.ts b/src/extension.ts index 8026442..df82ec9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -14,6 +14,7 @@ export function activate(context: vscode.ExtensionContext) { enable: config.get('enable'), referencesLabel: config.get('referencesLabel'), zeroReferencesLabel: config.get('zeroReferencesLabel'), + ignoredMethods: config.get('ignoredMethods'), cacheMaxEntries: config.get('cacheMaxEntries'), maxCachedLocations: config.get('maxCachedLocations'), cacheTtlSeconds: config.get('cacheTtlSeconds'), diff --git a/src/referenceLensProvider.ts b/src/referenceLensProvider.ts index 0155b92..5f336f3 100644 --- a/src/referenceLensProvider.ts +++ b/src/referenceLensProvider.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { CachedReferences, ReferenceCache } from './core/referenceCache'; import { DocumentDiagnosticsStore } from './core/diagnosticsStore'; -import { collectRelevantSymbols } from './core/symbols'; +import { collectRelevantSymbols, symbolKinds } from './core/symbols'; import { countNonDeclarationReferences, LocationLike } from './core/referenceCount'; import { DartReferencesSettings, formatLensTitle } from './core/settings'; import { shouldFlagUnused, unusedSymbolMessage } from './core/unused'; @@ -56,7 +56,8 @@ export class ReferenceCountCodeLensProvider implements vscode.CodeLensProvider { } async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { - if (!this.deps.getSettings().enabled) { + const settings = this.deps.getSettings(); + if (!settings.enabled) { this.diagnostics.clearDocument(document.uri.toString()); this.deps.setDiagnostics(document.uri, []); return []; @@ -72,6 +73,9 @@ export class ReferenceCountCodeLensProvider implements vscode.CodeLensProvider { const lenses: vscode.CodeLens[] = []; for (const symbol of collectRelevantSymbols(symbols)) { + if (symbol.kind === symbolKinds.method && settings.ignoredMethods.has(symbol.name)) { + continue; + } const lineStart = new vscode.Position(symbol.selectionRange.start.line, 0); const lens = new vscode.CodeLens(new vscode.Range(lineStart, lineStart)); this.lensMetadata.set(lens, { diff --git a/src/test/unit/referenceLensProvider.test.ts b/src/test/unit/referenceLensProvider.test.ts index 8858a9a..23c8c6e 100644 --- a/src/test/unit/referenceLensProvider.test.ts +++ b/src/test/unit/referenceLensProvider.test.ts @@ -82,6 +82,16 @@ describe('ReferenceCountCodeLensProvider', () => { ); }); + it('skips methods listed in ignoredMethods but not functions sharing the name', async () => { + symbolsResult = [ + fakeSymbol('build', SymbolKind.Method, 2), + fakeSymbol('build', SymbolKind.Function, 5), + fakeSymbol('helper', SymbolKind.Method, 8), + ]; + const lenses = await provider.provideCodeLenses(document(), token()); + assert.deepStrictEqual(lenses.map(l => l.range.start.line), [5, 8]); + }); + it('returns no lenses and clears diagnostics when disabled', async () => { settings = normalizeSettings({ enable: false }); const lenses = await provider.provideCodeLenses(document(), token()); diff --git a/src/test/unit/settings.test.ts b/src/test/unit/settings.test.ts index f0b2046..11cc8df 100644 --- a/src/test/unit/settings.test.ts +++ b/src/test/unit/settings.test.ts @@ -7,6 +7,21 @@ describe('normalizeSettings', () => { enabled: true, referencesLabel: 'references', zeroReferencesLabel: 'No references', + ignoredMethods: new Set([ + 'build', + 'initState', + 'dispose', + 'didChangeDependencies', + 'didUpdateWidget', + 'reassemble', + 'deactivate', + 'setState', + 'createState', + 'toString', + 'hashCode', + 'operator==', + 'noSuchMethod', + ]), cacheMaxEntries: 2000, maxCachedLocations: 1000, cacheTtlMs: 60_000, @@ -56,6 +71,11 @@ describe('normalizeSettings', () => { assert.strictEqual(normalizeSettings({ cacheMaxEntries: 250.7 }).cacheMaxEntries, 250); }); + it('uses the configured ignoredMethods list, dropping non-string entries', () => { + const settings = normalizeSettings({ ignoredMethods: ['mine', 42] }); + assert.deepStrictEqual([...settings.ignoredMethods], ['mine']); + }); + it('falls back to defaults for negative or non-numeric limits', () => { const settings = normalizeSettings({ cacheMaxEntries: -5, maxCachedLocations: 'lots', cacheTtlSeconds: NaN }); assert.strictEqual(settings.cacheMaxEntries, 2000);