Skip to content

Commit 9c532b5

Browse files
committed
fix: show locationless entries
1 parent 2e836e8 commit 9c532b5

File tree

3 files changed

+145
-63
lines changed

3 files changed

+145
-63
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ Findings and notes show the author's username after the filename/line number in
154154
### Third-party .weaudit compatibility
155155

156156
weAudit can read `.weaudit` files generated by third-party tools. If a file omits `auditedFiles` or `resolvedEntries`, weAudit treats them as empty so findings can still load.
157+
Entries without any locations are shown in the tree, but they cannot be navigated or have permalinks until a location is added.
157158

158159
### Hide Findings
159160
You can hide all findings associated with a specific user by clicking on that user's name on the `weAudit Files` panel.

src/codeMarker.ts

Lines changed: 131 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ import { normalizePathForOS } from "./utilities/normalizePath";
5252

5353
export const SERIALIZED_FILE_EXTENSION = ".weaudit";
5454
const DAY_LOG_FILENAME = ".weauditdaylog";
55+
// Special path label used to group entries that have no locations.
56+
const NO_LOCATION_PATH_LABEL = "(no location)";
5557

5658
/**
5759
* Class representing a WeAudit workspace root. Each root maintains its own set of
@@ -1611,6 +1613,8 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
16111613
private pathToEntryMap: Map<string, FullLocationEntry[]>;
16121614
private pathToEntryMapDirty = true;
16131615
private locationEntryCache = new WeakMap<FullLocation, FullLocationEntry>();
1616+
// Preserve which workspace root produced locationless entries so they can be filtered and saved correctly.
1617+
private entryOriginRoots = new WeakMap<FullEntry, RootPathAndLabel>();
16141618

16151619
private treeViewMode: TreeViewMode;
16161620

@@ -2158,6 +2162,11 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
21582162

21592163
async getCodeToCopyFromLocation(entry: FullEntry | FullLocationEntry): Promise<FromLocationResponse | void> {
21602164
const location = isLocationEntry(entry) ? entry.location : entry.locations[0];
2165+
if (location === undefined) {
2166+
// Locationless entries have no source code region.
2167+
vscode.window.showErrorMessage("weAudit: Cannot copy code for an entry without locations.");
2168+
return;
2169+
}
21612170
const permalink = await this.getClientPermalink(location);
21622171
if (permalink === undefined) {
21632172
return;
@@ -2684,6 +2693,11 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
26842693
*/
26852694
async copyEntryPermalink(entry: FullEntry | FullLocationEntry): Promise<void> {
26862695
const location = isLocationEntry(entry) ? entry.location : entry.locations[0];
2696+
if (location === undefined) {
2697+
// Locationless entries have no permalink target.
2698+
vscode.window.showErrorMessage("weAudit: Cannot copy a permalink for an entry without locations.");
2699+
return;
2700+
}
26872701
const remoteAndPermalink = await this.getEntryRemoteAndPermalink(location);
26882702
if (remoteAndPermalink === undefined) {
26892703
return;
@@ -2696,6 +2710,11 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
26962710
* @param entry The entry to copy the permalinks of
26972711
*/
26982712
async copyEntryPermalinks(entry: FullEntry): Promise<void> {
2713+
if (entry.locations.length === 0) {
2714+
// Locationless entries have no permalinks to gather.
2715+
vscode.window.showErrorMessage("weAudit: Cannot copy permalinks for an entry without locations.");
2716+
return;
2717+
}
26992718
const permalinkList = [];
27002719
for (const location of entry.locations) {
27012720
const remoteAndPermalink = await this.getEntryRemoteAndPermalink(location);
@@ -2750,6 +2769,11 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
27502769
* @param entry The entry to open an issue for
27512770
*/
27522771
async openGithubIssue(entry: FullEntry): Promise<void> {
2772+
if (entry.locations.length === 0) {
2773+
// Locationless entries cannot resolve a repository or file target.
2774+
vscode.window.showErrorMessage("weAudit: Cannot open a GitHub issue for an entry without locations.");
2775+
return;
2776+
}
27532777
// open github issue with the issue body with the finding text and permalink
27542778
const title = encodeURIComponent(entry.label);
27552779

@@ -3338,26 +3362,9 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
33383362
* @returns
33393363
*/
33403364
getFilteredEntriesForSaving(username: string, root: WARoot): [FullEntry[], FullEntry[]] {
3341-
const filteredEntries = this.treeEntries.filter((entry) => {
3342-
let inWs = false;
3343-
for (const location of entry.locations) {
3344-
if (location.rootPath === root.rootPath) {
3345-
inWs = true;
3346-
break;
3347-
}
3348-
}
3349-
return entry.author === username && inWs;
3350-
});
3351-
const filteredResolvedEntries = this.resolvedEntries.filter((entry) => {
3352-
let inWs = false;
3353-
for (const location of entry.locations) {
3354-
if (location.rootPath === root.rootPath) {
3355-
inWs = true;
3356-
break;
3357-
}
3358-
}
3359-
return entry.author === username && inWs;
3360-
});
3365+
// Include locationless entries that originated from this root when saving.
3366+
const filteredEntries = this.treeEntries.filter((entry) => entry.author === username && this.entryMatchesRoot(entry, root));
3367+
const filteredResolvedEntries = this.resolvedEntries.filter((entry) => entry.author === username && this.entryMatchesRoot(entry, root));
33613368
return [filteredEntries, filteredResolvedEntries];
33623369
}
33633370

@@ -3379,10 +3386,11 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
33793386
// create a quick pick to select the entry to add the region to
33803387
const items = this.treeEntries
33813388
.filter((entry) => {
3382-
if (entry.locations.length === 0 || entry.locations[0].rootPath !== locations[0].rootPath) {
3383-
return false;
3389+
if (entry.locations.length === 0) {
3390+
// Allow attaching the first location to entries imported without locations.
3391+
return true;
33843392
}
3385-
return true;
3393+
return entry.locations[0].rootPath === locations[0].rootPath;
33863394
})
33873395
.map((entry) => {
33883396
return {
@@ -3528,6 +3536,13 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
35283536
),
35293537
} as FullSerializedData;
35303538

3539+
// Keep track of which workspace root produced entries with no locations.
3540+
for (const entry of fullParsedEntries.treeEntries.concat(fullParsedEntries.resolvedEntries)) {
3541+
if (entry.locations.length === 0) {
3542+
this.entryOriginRoots.set(entry, { rootPath: rootPath, rootLabel: wsRoot.getRootLabel() });
3543+
}
3544+
}
3545+
35313546
// Normalize all the paths from loaded files. These can come from different OSes with different path
35323547
// conventions. We do a best effort to match them to the current OS format.
35333548
fullParsedEntries.treeEntries.forEach((entry) => {
@@ -3568,18 +3583,10 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
35683583
.map((selectedConfig) => selectedConfig.username)
35693584
.includes(config.username)
35703585
) {
3571-
this.treeEntries = this.treeEntries.filter(
3572-
(entry) =>
3573-
entry.author !== config.username ||
3574-
entry.locations.findIndex((loc) => this.workspaces.getUniqueLabel(loc.rootPath) !== config.root.label) !== -1,
3575-
);
3586+
this.treeEntries = this.treeEntries.filter((entry) => entry.author !== config.username || !this.entryMatchesConfig(entry, config));
35763587
wsRoot.filterAudited(config.username);
35773588
wsRoot.filterPartiallyAudited(config.username);
3578-
this.resolvedEntries = this.resolvedEntries.filter(
3579-
(entry) =>
3580-
entry.author !== config.username ||
3581-
entry.locations.findIndex((loc) => this.workspaces.getUniqueLabel(loc.rootPath) !== config.root.label) !== -1,
3582-
);
3589+
this.resolvedEntries = this.resolvedEntries.filter((entry) => entry.author !== config.username || !this.entryMatchesConfig(entry, config));
35833590
}
35843591

35853592
const newTreeEntries = fullParsedEntries.treeEntries;
@@ -3596,18 +3603,10 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
35963603
this.resolvedEntries = this.resolvedEntries.concat(fullParsedEntries.resolvedEntries);
35973604
}
35983605
} else {
3599-
this.treeEntries = this.treeEntries.filter(
3600-
(entry) =>
3601-
entry.author !== config.username ||
3602-
entry.locations.findIndex((loc) => this.workspaces.getUniqueLabel(loc.rootPath) !== config.root.label) !== -1,
3603-
);
3606+
this.treeEntries = this.treeEntries.filter((entry) => entry.author !== config.username || !this.entryMatchesConfig(entry, config));
36043607
wsRoot.filterAudited(config.username);
36053608
wsRoot.filterPartiallyAudited(config.username);
3606-
this.resolvedEntries = this.resolvedEntries.filter(
3607-
(entry) =>
3608-
entry.author !== config.username ||
3609-
entry.locations.findIndex((loc) => this.workspaces.getUniqueLabel(loc.rootPath) !== config.root.label) !== -1,
3610-
);
3609+
this.resolvedEntries = this.resolvedEntries.filter((entry) => entry.author !== config.username || !this.entryMatchesConfig(entry, config));
36113610
}
36123611
}
36133612

@@ -3721,18 +3720,10 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
37213720
if (wsRoot === undefined) {
37223721
return;
37233722
}
3724-
this.treeEntries = this.treeEntries.filter(
3725-
(entry) =>
3726-
entry.author !== config.username ||
3727-
entry.locations.findIndex((loc) => this.workspaces.getUniqueLabel(loc.rootPath) !== config.root.label) !== -1,
3728-
);
3723+
this.treeEntries = this.treeEntries.filter((entry) => entry.author !== config.username || !this.entryMatchesConfig(entry, config));
37293724
wsRoot.filterAudited(config.username);
37303725
wsRoot.filterPartiallyAudited(config.username);
3731-
this.resolvedEntries = this.resolvedEntries.filter(
3732-
(entry) =>
3733-
entry.author !== config.username ||
3734-
entry.locations.findIndex((loc) => this.workspaces.getUniqueLabel(loc.rootPath) !== config.root.label) !== -1,
3735-
);
3726+
this.resolvedEntries = this.resolvedEntries.filter((entry) => entry.author !== config.username || !this.entryMatchesConfig(entry, config));
37363727
this.markPathMapDirty();
37373728
}
37383729

@@ -3928,10 +3919,27 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
39283919

39293920
if (element === undefined) {
39303921
const pathLabels = Array.from(this.pathToEntryMap.keys()).sort();
3922+
const unlocatedEntries = this.treeEntries.filter((entry) => entry.locations.length === 0 && this.isEntryVisible(entry));
3923+
if (unlocatedEntries.length > 0) {
3924+
// Keep locationless entries visible in group-by-file mode under a dedicated bucket.
3925+
pathLabels.push(NO_LOCATION_PATH_LABEL);
3926+
}
39313927
return pathLabels.map((label) => createPathOrganizer(label));
39323928
} else {
39333929
// get entries with same path as element
39343930
if (isPathOrganizerEntry(element)) {
3931+
if (element.pathLabel === NO_LOCATION_PATH_LABEL) {
3932+
const unlocatedEntries = this.treeEntries.filter((entry) => entry.locations.length === 0 && this.isEntryVisible(entry));
3933+
if (this.sortEntriesAlphabetically) {
3934+
return [...unlocatedEntries].sort((a, b) => {
3935+
if (a.entryType !== b.entryType) {
3936+
return a.entryType === EntryType.Finding ? -1 : 1;
3937+
}
3938+
return a.label.localeCompare(b.label);
3939+
});
3940+
}
3941+
return unlocatedEntries;
3942+
}
39353943
const entries = this.pathToEntryMap.get(element.pathLabel) ?? [];
39363944
if (this.sortEntriesAlphabetically) {
39373945
return [...entries].sort((a, b) => {
@@ -3993,7 +4001,7 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
39934001
notes.sort((a, b) => a.label.localeCompare(b.label));
39944002
}
39954003

3996-
return entries.concat(notes).filter((entry) => this.hasVisibleLocation(entry));
4004+
return entries.concat(notes).filter((entry) => this.isEntryVisible(entry));
39974005
}
39984006

39994007
/**
@@ -4107,6 +4115,13 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
41074115
}
41084116

41094117
const mainLocation = entry.locations[0];
4118+
if (mainLocation === undefined) {
4119+
// Entries imported without locations still appear, but cannot be navigated.
4120+
treeItem.description = `No location (${entry.author})`;
4121+
treeItem.tooltip = `${entry.author}'s ${entry.entryType === EntryType.Note ? "note" : "finding"} (no location)`;
4122+
treeItem.contextValue = entry.entryType === EntryType.Note ? "note" : "finding";
4123+
return treeItem;
4124+
}
41104125

41114126
const basePath = path.basename(mainLocation.path);
41124127
treeItem.description = basePath + ":" + (mainLocation.startLine + 1).toString();
@@ -4244,6 +4259,51 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
42444259
return false;
42454260
}
42464261

4262+
/**
4263+
* Returns whether an entry should be shown in the tree view.
4264+
* Locationless entries are scoped to the config root they were loaded from.
4265+
*/
4266+
private isEntryVisible(entry: FullEntry): boolean {
4267+
if (entry.locations.length > 0) {
4268+
return this.hasVisibleLocation(entry);
4269+
}
4270+
const origin = this.entryOriginRoots.get(entry);
4271+
if (origin === undefined) {
4272+
return !this.workspaces.moreThanOneRoot() && this.workspaces.getSelectedConfigurations().some((config) => config.username === entry.author);
4273+
}
4274+
return this.workspaces.getSelectedConfigurations().some((config) => config.username === entry.author && config.root.label === origin.rootLabel);
4275+
}
4276+
4277+
/**
4278+
* Returns true when an entry belongs to the config's workspace root.
4279+
* Locationless entries use their recorded origin root.
4280+
*/
4281+
private entryMatchesConfig(entry: FullEntry, config: ConfigurationEntry): boolean {
4282+
if (entry.locations.length > 0) {
4283+
return entry.locations.findIndex((loc) => this.workspaces.getUniqueLabel(loc.rootPath) !== config.root.label) === -1;
4284+
}
4285+
const origin = this.entryOriginRoots.get(entry);
4286+
if (origin !== undefined) {
4287+
return origin.rootLabel === config.root.label;
4288+
}
4289+
return !this.workspaces.moreThanOneRoot();
4290+
}
4291+
4292+
/**
4293+
* Returns true when an entry should be saved under the provided workspace root.
4294+
* Locationless entries use their recorded origin root.
4295+
*/
4296+
private entryMatchesRoot(entry: FullEntry, root: WARoot): boolean {
4297+
if (entry.locations.length > 0) {
4298+
return entry.locations.some((location) => location.rootPath === root.rootPath);
4299+
}
4300+
const origin = this.entryOriginRoots.get(entry);
4301+
if (origin !== undefined) {
4302+
return origin.rootPath === root.rootPath;
4303+
}
4304+
return !this.workspaces.moreThanOneRoot();
4305+
}
4306+
42474307
private markPathMapDirty(): void {
42484308
this.pathToEntryMapDirty = true;
42494309
}
@@ -4288,6 +4348,11 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
42884348
* @param entry the entry to refresh and decorate
42894349
*/
42904350
refreshAndDecorateEntry(entry: FullEntry): void {
4351+
if (entry.locations.length === 0) {
4352+
// Ensure locationless entries still trigger a tree refresh.
4353+
this.refreshTree();
4354+
return;
4355+
}
42914356
for (const loc of entry.locations) {
42924357
const uri = vscode.Uri.file(path.join(loc.rootPath, loc.path));
42934358
this.decorateWithUri(uri);
@@ -4385,6 +4450,12 @@ class DragAndDropController implements vscode.TreeDragAndDropController<TreeEntr
43854450
return;
43864451
}
43874452

4453+
// Locationless entries cannot accept dropped locations.
4454+
if (target.locations.length === 0) {
4455+
vscode.window.showErrorMessage("weAudit: Error moving a location to a finding without locations.");
4456+
return;
4457+
}
4458+
43884459
// Prevent mixing findings that belong to different workspace roots, because it is a headache to synchronize this.
43894460
if (target.locations[0].rootPath !== locationEntry.location.rootPath) {
43904461
vscode.window.showErrorMessage(
@@ -4499,6 +4570,12 @@ class DragAndDropController implements vscode.TreeDragAndDropController<TreeEntr
44994570
target = target.parentEntry;
45004571
}
45014572

4573+
// Entries without locations cannot be merged because we cannot verify roots.
4574+
if (entry.locations.length === 0 || target.locations.length === 0) {
4575+
vscode.window.showErrorMessage("weAudit: Error merging findings that do not have locations.");
4576+
return;
4577+
}
4578+
45024579
// Prevent mixing findings that belong to different workspace roots, because it is a headache to synchronize this.
45034580
if (target.locations[0].rootPath !== entry.locations[0].rootPath) {
45044581
vscode.window.showErrorMessage("weAudit: Error merging findings, as this finding is in a different workspace root.");

src/resolvedFindings.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,20 @@ export class ResolvedEntriesTree implements vscode.TreeDataProvider<FullEntry> {
8989
} else {
9090
treeItem.iconPath = new vscode.ThemeIcon("bug");
9191
}
92-
const mainLocation = entry.locations[0];
93-
treeItem.command = {
94-
command: "weAudit.openFileLines",
95-
title: "Open File",
96-
arguments: [vscode.Uri.file(path.join(mainLocation.rootPath, mainLocation.path)), mainLocation.startLine, mainLocation.endLine],
97-
};
98-
9992
const badge = getResolutionBadge(entry);
100-
treeItem.description = `${path.basename(mainLocation.path)} [${badge}]`;
101-
treeItem.tooltip = `${entry.author}'s ${entry.entryType === EntryType.Note ? "note" : "finding"} (${badge})`;
93+
const mainLocation = entry.locations[0];
94+
if (mainLocation !== undefined) {
95+
treeItem.command = {
96+
command: "weAudit.openFileLines",
97+
title: "Open File",
98+
arguments: [vscode.Uri.file(path.join(mainLocation.rootPath, mainLocation.path)), mainLocation.startLine, mainLocation.endLine],
99+
};
100+
treeItem.description = `${path.basename(mainLocation.path)} [${badge}]`;
101+
treeItem.tooltip = `${entry.author}'s ${entry.entryType === EntryType.Note ? "note" : "finding"} (${badge})`;
102+
} else {
103+
treeItem.description = `No location [${badge}]`;
104+
treeItem.tooltip = `${entry.author}'s ${entry.entryType === EntryType.Note ? "note" : "finding"} (${badge}, no location)`;
105+
}
102106
treeItem.contextValue = entry.entryType === EntryType.Note ? "resolvedNote" : "resolvedFinding";
103107

104108
return treeItem;

0 commit comments

Comments
 (0)