diff --git a/defects/vscode/patch/vscode-0001-gallery-uuid-includes.patch b/defects/vscode/patch/vscode-0001-gallery-uuid-includes.patch new file mode 100644 index 000000000..64aa88a21 --- /dev/null +++ b/defects/vscode/patch/vscode-0001-gallery-uuid-includes.patch @@ -0,0 +1,25 @@ +# UNDF: (leave blank) +# vscode-0001: extensionGalleryService.getExtensions uuid dedup O(N×M) +# +# File: src/vs/platform/extensionManagement/common/extensionGalleryService.ts +# Function: getExtensions (line ~659) +# +# Defect: uuids array built via result.map(), then for each extensionInfo +# uuids.includes(e.uuid) is called — O(N×M) where N=extensionInfos, +# M=result count. +# Fix: Convert uuids to a Set for O(1) lookup. +# Severity: MEDIUM (N=installed extensions, typically 50-200) +# Speedup: 100x at N=200 +# +--- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts ++++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +@@ -659,7 +659,7 @@ +- const uuids = result.map(r => r.identifier.uuid); ++ const uuids = new Set(result.map(r => r.identifier.uuid)); + const extensionInfosByName: IExtensionInfo[] = []; + for (const e of extensionInfos) { +- if (e.uuid && !uuids.includes(e.uuid)) { ++ if (e.uuid && !uuids.has(e.uuid)) { + extensionInfosByName.push({ ...e, uuid: undefined }); + } + } diff --git a/defects/vscode/patch/vscode-0002-extension-deps-includes.patch b/defects/vscode/patch/vscode-0002-extension-deps-includes.patch new file mode 100644 index 000000000..49fa8d65e --- /dev/null +++ b/defects/vscode/patch/vscode-0002-extension-deps-includes.patch @@ -0,0 +1,40 @@ +# UNDF: (leave blank) +# vscode-0002: abstractExtensionManagementService.getAllDepsAndPacks O(D²) +# +# File: src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +# Function: getAllDepsAndPacks (line ~448) +# +# Defect: Recursive dependency traversal accumulates IDs into allDepsOrPacks +# array, checking allDepsOrPacks.includes(id) on every iteration — +# O(D²) where D = total transitive dependencies+packs. +# Fix: Use a Set for the seen-check; keep the array for the result list. +# Severity: MEDIUM (D = extension dependency tree depth × breadth, typically 10-50) +# Speedup: 25x at D=50 +# +--- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts ++++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +@@ -448,7 +448,8 @@ +- const getAllDepsAndPacks = (extension: ILocalExtension, profileLocation: URI, allDepsOrPacks: string[]) => { ++ const getAllDepsAndPacks = (extension: ILocalExtension, profileLocation: URI, allDepsOrPacks: string[], seenDepsOrPacks?: Set) => { ++ if (!seenDepsOrPacks) { seenDepsOrPacks = new Set(allDepsOrPacks); } + const depsOrPacks = []; + if (extension.manifest.extensionDependencies?.length) { + depsOrPacks.push(...extension.manifest.extensionDependencies); +@@ -456,12 +457,13 @@ + for (const id of depsOrPacks) { +- if (allDepsOrPacks.includes(id.toLowerCase())) { ++ const lowerId = id.toLowerCase(); ++ if (seenDepsOrPacks.has(lowerId)) { + continue; + } +- allDepsOrPacks.push(id.toLowerCase()); +- const installed = installExtensionResultsMap.get(`${id.toLowerCase()}-${profileLocation.toString()}`); ++ seenDepsOrPacks.add(lowerId); ++ allDepsOrPacks.push(lowerId); ++ const installed = installExtensionResultsMap.get(`${lowerId}-${profileLocation.toString()}`); + if (installed?.local) { +- allDepsOrPacks = getAllDepsAndPacks(installed.local, profileLocation, allDepsOrPacks); ++ allDepsOrPacks = getAllDepsAndPacks(installed.local, profileLocation, allDepsOrPacks, seenDepsOrPacks); + } + } + return allDepsOrPacks; diff --git a/defects/vscode/patch/vscode-0003-sync-merge-compare-includes.patch b/defects/vscode/patch/vscode-0003-sync-merge-compare-includes.patch new file mode 100644 index 000000000..905327e15 --- /dev/null +++ b/defects/vscode/patch/vscode-0003-sync-merge-compare-includes.patch @@ -0,0 +1,38 @@ +# UNDF: (leave blank) +# vscode-0003: userDataSync merge compare() functions O(N×M) array includes +# +# Files affected (all same pattern): +# src/vs/platform/userDataSync/common/extensionsMerge.ts (lines 259-260, 375-376) +# src/vs/platform/userDataSync/common/settingsMerge.ts (lines 301-302) +# src/vs/platform/userDataSync/common/globalStateMerge.ts (lines 126-127) +# src/vs/platform/userDataSync/common/snippetsMerge.ts (lines 156-157) +# src/vs/platform/userDataSync/common/keybindingsMerge.ts (lines 258-259, 279-280) +# src/vs/platform/userDataSync/common/promptsSync/promptsMerge.ts (lines 156-157) +# src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts (lines 118-119) +# +# Defect: Every compare() function computes set-difference between two key +# arrays using fromKeys.filter(key => !toKeys.includes(key)) — O(N×M) +# where N, M = number of keys in each side. Runs on every sync merge. +# Fix: Convert one side to a Set before filtering. The result is already +# fed into a Set, so intermediate Set creation is free. +# Severity: MEDIUM (N = settings/extensions/keybindings count, 100-500) +# Speedup: 250x at N=500 +# +# Representative fix for extensionsMerge.ts compare(): +--- a/src/vs/platform/userDataSync/common/extensionsMerge.ts ++++ b/src/vs/platform/userDataSync/common/extensionsMerge.ts +@@ -256,8 +256,10 @@ + function compare(from: Map | null, to: Map, ignoredExtensions: Set, checkVersionProperty: boolean): { added: Set; removed: Set; updated: Set } { + const fromKeys = from ? [...from.keys()].filter(key => !ignoredExtensions.has(key)) : []; + const toKeys = [...to.keys()].filter(key => !ignoredExtensions.has(key)); +- const added = toKeys.filter(key => !fromKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set()); +- const removed = fromKeys.filter(key => !toKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set()); ++ const fromKeysSet = new Set(fromKeys); ++ const toKeysSet = new Set(toKeys); ++ const added = toKeys.filter(key => !fromKeysSet.has(key)).reduce((r, key) => { r.add(key); return r; }, new Set()); ++ const removed = fromKeys.filter(key => !toKeysSet.has(key)).reduce((r, key) => { r.add(key); return r; }, new Set()); + const updated: Set = new Set(); + +# Same fix applies to all 7 other compare() call sites listed above. +# Each instance: replace array.includes() with Set.has() by converting +# the lookup array to a Set before the filter call. diff --git a/defects/vscode/patch/vscode-0004-config-override-identifiers-includes.patch b/defects/vscode/patch/vscode-0004-config-override-identifiers-includes.patch new file mode 100644 index 000000000..262b3c8fa --- /dev/null +++ b/defects/vscode/patch/vscode-0004-config-override-identifiers-includes.patch @@ -0,0 +1,44 @@ +# UNDF: (leave blank) +# vscode-0004: configurationModels override identifiers O(N×M) array includes +# +# File: src/vs/platform/configuration/common/configurationModels.ts +# Function: compare (line ~1248) +# +# Defect: Override identifiers are arrays; set-difference computed with +# filter + includes — O(N×M) where N, M = number of override +# identifiers (language-specific settings like [typescript], [python]). +# Plus line 1267: inner loop also uses toOverrideIdentifiers.includes(). +# Fix: Convert to Sets for O(1) lookup. +# Severity: LOW-MEDIUM (N = language overrides, typically 5-30) +# Speedup: 30x at N=30 +# +--- a/src/vs/platform/configuration/common/configurationModels.ts ++++ b/src/vs/platform/configuration/common/configurationModels.ts +@@ -1248,17 +1248,19 @@ + const fromOverrideIdentifiers = from?.getAllOverrideIdentifiers() || []; + const toOverrideIdentifiers = to?.getAllOverrideIdentifiers() || []; ++ const fromOverrideSet = new Set(fromOverrideIdentifiers); ++ const toOverrideSet = new Set(toOverrideIdentifiers); + + if (to) { +- const addedOverrideIdentifiers = toOverrideIdentifiers.filter(key => !fromOverrideIdentifiers.includes(key)); ++ const addedOverrideIdentifiers = toOverrideIdentifiers.filter(key => !fromOverrideSet.has(key)); + for (const identifier of addedOverrideIdentifiers) { + overrides.push([identifier, to.getKeysForOverrideIdentifier(identifier)]); + } + } + + if (from) { +- const removedOverrideIdentifiers = fromOverrideIdentifiers.filter(key => !toOverrideIdentifiers.includes(key)); ++ const removedOverrideIdentifiers = fromOverrideIdentifiers.filter(key => !toOverrideSet.has(key)); + for (const identifier of removedOverrideIdentifiers) { + overrides.push([identifier, from.getKeysForOverrideIdentifier(identifier)]); + } +@@ -1265,7 +1267,7 @@ + if (to && from) { + for (const identifier of fromOverrideIdentifiers) { +- if (toOverrideIdentifiers.includes(identifier)) { ++ if (toOverrideSet.has(identifier)) { + const result = compareConfigurationContents({ contents: from.getOverrideValue(undefined, identifier) || {}, keys: from.getKeysForOverrideIdentifier(identifier) }, { contents: to.getOverrideValue(undefined, identifier) || {}, keys: to.getKeysForOverrideIdentifier(identifier) }); + overrides.push([identifier, [...result.added, ...result.removed, ...result.updated]]); + } diff --git a/defects/vscode/unit/VscodeTest.java b/defects/vscode/unit/VscodeTest.java new file mode 100644 index 000000000..473f1ea26 --- /dev/null +++ b/defects/vscode/unit/VscodeTest.java @@ -0,0 +1,226 @@ +import java.util.*; + +/** + * CWE-407 unit tests for VS Code defects. + * + * vscode-0001: extensionGalleryService getExtensions uuid dedup O(N×M) + * vscode-0002: abstractExtensionManagementService getAllDepsAndPacks O(D²) + * vscode-0003: userDataSync merge compare() O(N×M) array includes (8 sites) + * vscode-0004: configurationModels override identifiers O(N×M) + */ +public class VscodeTest { + + // --------------------------------------------------------------- + // vscode-0001: gallery uuid includes O(N×M) + // --------------------------------------------------------------- + + /** BEFORE: uuids array with Array.includes() — O(N×M) */ + static int galleryUuidBefore(List extensionUuids, List resultUuids) { + int ops = 0; + // const uuids = result.map(r => r.identifier.uuid); // array + for (String uuid : extensionUuids) { + // uuids.includes(e.uuid) + for (String r : resultUuids) { + ops++; + if (r.equals(uuid)) break; + } + } + return ops; + } + + /** AFTER: uuids Set with Set.has() — O(N+M) */ + static int galleryUuidAfter(List extensionUuids, List resultUuids) { + int ops = 0; + Set uuidSet = new HashSet<>(resultUuids); + ops += resultUuids.size(); // set construction + for (String uuid : extensionUuids) { + ops++; // set lookup O(1) + uuidSet.contains(uuid); + } + return ops; + } + + // --------------------------------------------------------------- + // vscode-0002: getAllDepsAndPacks includes O(D²) + // --------------------------------------------------------------- + + /** BEFORE: allDepsOrPacks.includes() in recursive traversal — O(D²) */ + static int depsScanBefore(List depChain) { + int ops = 0; + List allDeps = new ArrayList<>(); + for (String id : depChain) { + // allDepsOrPacks.includes(id.toLowerCase()) + boolean found = false; + for (String existing : allDeps) { + ops++; + if (existing.equals(id)) { found = true; break; } + } + if (!found) allDeps.add(id); + } + return ops; + } + + /** AFTER: Set for seen-check — O(D) */ + static int depsScanAfter(List depChain) { + int ops = 0; + Set seen = new HashSet<>(); + List allDeps = new ArrayList<>(); + for (String id : depChain) { + ops++; // set lookup + if (!seen.contains(id)) { + seen.add(id); + allDeps.add(id); + } + } + return ops; + } + + // --------------------------------------------------------------- + // vscode-0003: sync merge compare() — filter+includes O(N×M) + // --------------------------------------------------------------- + + /** BEFORE: fromKeys.filter(key => !toKeys.includes(key)) — O(N×M) */ + static int mergeCompareBefore(List fromKeys, List toKeys) { + int ops = 0; + // added = toKeys.filter(key => !fromKeys.includes(key)) + for (String key : toKeys) { + for (String fk : fromKeys) { + ops++; + if (fk.equals(key)) break; + } + } + // removed = fromKeys.filter(key => !toKeys.includes(key)) + for (String key : fromKeys) { + for (String tk : toKeys) { + ops++; + if (tk.equals(key)) break; + } + } + return ops; + } + + /** AFTER: Set lookup — O(N+M) */ + static int mergeCompareAfter(List fromKeys, List toKeys) { + int ops = 0; + Set fromSet = new HashSet<>(fromKeys); + ops += fromKeys.size(); + Set toSet = new HashSet<>(toKeys); + ops += toKeys.size(); + for (String key : toKeys) { ops++; fromSet.contains(key); } + for (String key : fromKeys) { ops++; toSet.contains(key); } + return ops; + } + + // --------------------------------------------------------------- + // vscode-0004: config override identifiers — filter+includes O(N×M) + // --------------------------------------------------------------- + + /** BEFORE: overrideIdentifiers.filter + includes — O(N²) */ + static int configOverrideBefore(List fromIds, List toIds) { + int ops = 0; + // addedOverrideIdentifiers = toIds.filter(key => !fromIds.includes(key)) + for (String key : toIds) { + for (String fk : fromIds) { ops++; if (fk.equals(key)) break; } + } + // removedOverrideIdentifiers = fromIds.filter(key => !toIds.includes(key)) + for (String key : fromIds) { + for (String tk : toIds) { ops++; if (tk.equals(key)) break; } + } + // inner loop: toOverrideIdentifiers.includes(identifier) + for (String id : fromIds) { + for (String tk : toIds) { ops++; if (tk.equals(id)) break; } + } + return ops; + } + + /** AFTER: Set lookups — O(N+M) */ + static int configOverrideAfter(List fromIds, List toIds) { + int ops = 0; + Set fromSet = new HashSet<>(fromIds); + ops += fromIds.size(); + Set toSet = new HashSet<>(toIds); + ops += toIds.size(); + for (String key : toIds) { ops++; fromSet.contains(key); } + for (String key : fromIds) { ops++; toSet.contains(key); } + for (String id : fromIds) { ops++; toSet.contains(id); } + return ops; + } + + // --------------------------------------------------------------- + // Main: run all tests + // --------------------------------------------------------------- + public static void main(String[] args) { + int pass = 0, fail = 0; + + // --- vscode-0001: gallery uuid --- + { + int N = 200; + List extUuids = new ArrayList<>(); + List resultUuids = new ArrayList<>(); + for (int i = 0; i < N; i++) { + extUuids.add("ext-" + i); + resultUuids.add("res-" + i); // disjoint = worst case + } + int before = galleryUuidBefore(extUuids, resultUuids); + int after = galleryUuidAfter(extUuids, resultUuids); + double ratio = (double) before / after; + boolean ok = ratio > 10; + System.out.printf("vscode-0001 gallery-uuid N=%d before=%d after=%d ratio=%.1fx %s%n", + N, before, after, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // --- vscode-0002: deps scan --- + { + int D = 100; + List deps = new ArrayList<>(); + for (int i = 0; i < D; i++) deps.add("dep-" + i); + int before = depsScanBefore(deps); + int after = depsScanAfter(deps); + double ratio = (double) before / after; + boolean ok = ratio > 10; + System.out.printf("vscode-0002 deps-scan D=%d before=%d after=%d ratio=%.1fx %s%n", + D, before, after, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // --- vscode-0003: sync merge compare --- + { + int N = 500; + List fromKeys = new ArrayList<>(); + List toKeys = new ArrayList<>(); + for (int i = 0; i < N; i++) { + fromKeys.add("setting-" + i); + toKeys.add("setting-" + (i + N)); // disjoint = worst case + } + int before = mergeCompareBefore(fromKeys, toKeys); + int after = mergeCompareAfter(fromKeys, toKeys); + double ratio = (double) before / after; + boolean ok = ratio > 50; + System.out.printf("vscode-0003 sync-merge-compare N=%d before=%d after=%d ratio=%.1fx %s%n", + N, before, after, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // --- vscode-0004: config override identifiers --- + { + int N = 30; + List fromIds = new ArrayList<>(); + List toIds = new ArrayList<>(); + for (int i = 0; i < N; i++) { + fromIds.add("[lang-" + i + "]"); + toIds.add("[lang-" + (i + N) + "]"); // disjoint + } + int before = configOverrideBefore(fromIds, toIds); + int after = configOverrideAfter(fromIds, toIds); + double ratio = (double) before / after; + boolean ok = ratio > 5; + System.out.printf("vscode-0004 config-overrides N=%d before=%d after=%d ratio=%.1fx %s%n", + N, before, after, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + System.out.printf("%n%d/%d PASS%n", pass, pass + fail); + if (fail > 0) System.exit(1); + } +} diff --git a/defects/zed/patch/zed-0001-lsp-edit-dedup-contains.patch b/defects/zed/patch/zed-0001-lsp-edit-dedup-contains.patch new file mode 100644 index 000000000..176295d16 --- /dev/null +++ b/defects/zed/patch/zed-0001-lsp-edit-dedup-contains.patch @@ -0,0 +1,65 @@ +# UNDF: (leave blank) +# zed-0001: lsp_store LSP edit dedup Vec::contains O(E²) +# +# File: crates/project/src/lsp_store.rs +# Functions: apply_code_actions_as_format (line ~2010) and +# apply_code_action (line ~3356) +# +# Defect: When accumulating unique edits from LSP code actions, each new +# edit is checked against the `lsp_edits`/`edits` Vec via +# Vec::contains() — O(E²) where E = number of edits returned by +# the language server. Both call sites use the same pattern: +# +# let mut lsp_edits = Vec::new(); +# for edit in op.edits { +# if !lsp_edits.contains(&edit) { lsp_edits.push(edit); } +# } +# +# Fix: Use a HashSet as a parallel seen-set for O(1) dedup, keeping Vec +# for ordered output. +# Severity: MEDIUM (E = edits per code action; large refactors can produce +# hundreds of edits — e.g., rename across file) +# Speedup: 50x at E=100 +# +--- a/crates/project/src/lsp_store.rs ++++ b/crates/project/src/lsp_store.rs +@@ -2010,10 +2010,12 @@ (apply_code_actions_as_format, first site) + let mut lsp_edits = Vec::new(); ++ let mut seen_edits = HashSet::new(); + for edit in op.edits { + match edit { + Edit::Plain(edit) => { +- if !lsp_edits.contains(&edit) { ++ if seen_edits.insert(edit.clone()) { + lsp_edits.push(edit); + } + } + Edit::Annotated(edit) => { +- if !lsp_edits.contains(&edit.text_edit) { ++ if seen_edits.insert(edit.text_edit.clone()) { + lsp_edits.push(edit.text_edit); + } + } + +@@ -3356,10 +3358,12 @@ (apply_code_action, second site) + let (mut edits, mut snippet_edits) = (vec![], vec![]); ++ let mut seen_edits = HashSet::new(); + for edit in op.edits { + match edit { + Edit::Plain(edit) => { +- if !edits.contains(&edit) { ++ if seen_edits.insert(edit.clone()) { + edits.push(edit) + } + } + Edit::Annotated(edit) => { +- if !edits.contains(&edit.text_edit) { ++ if seen_edits.insert(edit.text_edit.clone()) { + edits.push(edit.text_edit) + } + } +@@ -3383,1 +3385,1 @@ +- if !edits.contains(&new_edit) { ++ if seen_edits.insert(new_edit.clone()) { + edits.push(new_edit); + } diff --git a/defects/zed/patch/zed-0002-extension-manifest-dedup.patch b/defects/zed/patch/zed-0002-extension-manifest-dedup.patch new file mode 100644 index 000000000..c38232ff3 --- /dev/null +++ b/defects/zed/patch/zed-0002-extension-manifest-dedup.patch @@ -0,0 +1,55 @@ +# UNDF: (leave blank) +# zed-0002: extension_builder manifest entry dedup Vec::contains O(N²) +# +# File: crates/extension/src/extension_builder.rs +# Function: compile_extension_manifest (lines 585-632) +# +# Defect: Three while-loops accumulate entries into manifest.languages, +# manifest.themes, and manifest.icon_themes (all Vec), each +# checking Vec::contains before push — O(N²) per category where +# N = number of language dirs / theme files / icon theme files. +# +# Fix: Use a HashSet as a seen-set for each category. +# Severity: LOW (N = extension assets, typically 1-20; only at build time) +# Speedup: 20x at N=20 +# +--- a/crates/extension/src/extension_builder.rs ++++ b/crates/extension/src/extension_builder.rs +@@ -580,6 +580,7 @@ ++ let existing_languages: HashSet<_> = manifest.languages.iter().cloned().collect(); + while let Some(language_dir) = language_dir_entries.next().await { + let language_dir = language_dir?; + let config_path = language_dir.join(LanguageConfig::FILE_NAME); + if fs.is_file(config_path.as_path()).await { + let relative_language_dir = + language_dir.strip_prefix(extension_path)?.to_path_buf(); +- if !manifest.languages.contains(&relative_language_dir) { ++ if !existing_languages.contains(&relative_language_dir) { + manifest.languages.push(relative_language_dir); + } + } + +@@ -598,6 +600,7 @@ ++ let existing_themes: HashSet<_> = manifest.themes.iter().cloned().collect(); + while let Some(theme_path) = theme_dir_entries.next().await { + let theme_path = theme_path?; + if theme_path.extension() == Some("json".as_ref()) { + let relative_theme_path = theme_path.strip_prefix(extension_path)?.to_path_buf(); +- if !manifest.themes.contains(&relative_theme_path) { ++ if !existing_themes.contains(&relative_theme_path) { + manifest.themes.push(relative_theme_path); + } + } + +@@ -616,6 +620,7 @@ ++ let existing_icon_themes: HashSet<_> = manifest.icon_themes.iter().cloned().collect(); + while let Some(icon_theme_path) = icon_theme_dir_entries.next().await { + let icon_theme_path = icon_theme_path?; + if icon_theme_path.extension() == Some("json".as_ref()) { + let relative_icon_theme_path = + icon_theme_path.strip_prefix(extension_path)?.to_path_buf(); +- if !manifest.icon_themes.contains(&relative_icon_theme_path) { ++ if !existing_icon_themes.contains(&relative_icon_theme_path) { + manifest.icon_themes.push(relative_icon_theme_path); + } + } diff --git a/defects/zed/unit/ZedTest.java b/defects/zed/unit/ZedTest.java new file mode 100644 index 000000000..5c0fbd6a5 --- /dev/null +++ b/defects/zed/unit/ZedTest.java @@ -0,0 +1,115 @@ +import java.util.*; + +/** + * CWE-407 unit tests for Zed editor defects. + * + * zed-0001: lsp_store LSP edit dedup Vec::contains O(E²) + * zed-0002: extension_builder manifest entry dedup Vec::contains O(N²) + */ +public class ZedTest { + + // --------------------------------------------------------------- + // zed-0001: LSP edit dedup Vec::contains O(E²) + // --------------------------------------------------------------- + + /** BEFORE: Vec::contains for each new edit — O(E²) */ + static int lspEditDedupBefore(List edits) { + int ops = 0; + List lspEdits = new ArrayList<>(); + for (String edit : edits) { + // !lsp_edits.contains(&edit) + boolean found = false; + for (String existing : lspEdits) { + ops++; + if (existing.equals(edit)) { found = true; break; } + } + if (!found) lspEdits.add(edit); + } + return ops; + } + + /** AFTER: HashSet seen-set for O(1) dedup — O(E) */ + static int lspEditDedupAfter(List edits) { + int ops = 0; + Set seen = new HashSet<>(); + List lspEdits = new ArrayList<>(); + for (String edit : edits) { + ops++; // set lookup + if (seen.add(edit)) { + lspEdits.add(edit); + } + } + return ops; + } + + // --------------------------------------------------------------- + // zed-0002: extension manifest dedup Vec::contains O(N²) + // --------------------------------------------------------------- + + /** BEFORE: manifest.languages.contains for each dir — O(N²) */ + static int manifestDedupBefore(List entries) { + int ops = 0; + List manifest = new ArrayList<>(); + for (String entry : entries) { + boolean found = false; + for (String existing : manifest) { + ops++; + if (existing.equals(entry)) { found = true; break; } + } + if (!found) manifest.add(entry); + } + return ops; + } + + /** AFTER: HashSet seen-set — O(N) */ + static int manifestDedupAfter(List entries) { + int ops = 0; + Set existing = new HashSet<>(); + List manifest = new ArrayList<>(); + for (String entry : entries) { + ops++; + if (existing.add(entry)) { + manifest.add(entry); + } + } + return ops; + } + + // --------------------------------------------------------------- + // Main: run all tests + // --------------------------------------------------------------- + public static void main(String[] args) { + int pass = 0, fail = 0; + + // --- zed-0001: LSP edit dedup --- + { + int E = 100; // edits from large refactor + List edits = new ArrayList<>(); + for (int i = 0; i < E; i++) edits.add("edit-" + i); // all unique = worst case + int before = lspEditDedupBefore(edits); + int after = lspEditDedupAfter(edits); + double ratio = (double) before / after; + boolean ok = ratio > 10; + System.out.printf("zed-0001 lsp-edit-dedup E=%d before=%d after=%d ratio=%.1fx %s%n", + E, before, after, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // --- zed-0002: manifest dedup --- + { + int N = 50; // language dirs in a large extension + List entries = new ArrayList<>(); + for (int i = 0; i < N; i++) entries.add("languages/lang-" + i); + int before = manifestDedupBefore(entries); + int after = manifestDedupAfter(entries); + double ratio = (double) before / after; + boolean ok = ratio > 10; + System.out.printf("zed-0002 manifest-dedup N=%d before=%d after=%d ratio=%.1fx %s%n", + N, before, after, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + System.out.printf("%n%d/%d PASS%n", pass, pass + fail); + if (fail > 0) System.exit(1); + } +}