Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
47 changes: 47 additions & 0 deletions lib/src/scan/extract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { assignOccurrenceIndexes } from "./extract";

const hit = (value: string, line: number, column = 1) => ({
value,
location: { line, column },
});

describe("assignOccurrenceIndexes", () => {
test("numbers repeats in source order", () => {
const hits = [hit("Save", 40, 8), hit("Cancel", 12), hit("Save", 9)];

expect(assignOccurrenceIndexes(hits)).toEqual([1, 0, 0]);
});

test("numbers repeats regardless of the order hits arrive in", () => {
const sourceOrder = [hit("Save", 9), hit("Save", 22), hit("Save", 40)];
const kindGrouped = [hit("Save", 40), hit("Save", 9), hit("Save", 22)];

expect(assignOccurrenceIndexes(sourceOrder)).toEqual([0, 1, 2]);
expect(assignOccurrenceIndexes(kindGrouped)).toEqual([2, 0, 1]);
});

test("is stable when lines shift", () => {
const before = [hit("Save", 9), hit("Save", 40, 8)];
const after = [hit("Save", 11), hit("Save", 42, 8)];

expect(assignOccurrenceIndexes(after)).toEqual(
assignOccurrenceIndexes(before)
);
});

test("counts each distinct value separately", () => {
const hits = [hit("Save", 1), hit("Cancel", 2), hit("Save", 3)];

expect(assignOccurrenceIndexes(hits)).toEqual([0, 0, 1]);
});

test("orders hits on the same line by column", () => {
const hits = [hit("Save", 5, 30), hit("Save", 5, 10)];

expect(assignOccurrenceIndexes(hits)).toEqual([1, 0]);
});

test("returns an empty result for no hits", () => {
expect(assignOccurrenceIndexes([])).toEqual([]);
});
});
37 changes: 32 additions & 5 deletions lib/src/scan/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import fs from "fs/promises";
import { globby } from "globby";
import path from "path";

import { createHash } from "crypto";
import type { FileDiscoveryStats } from "./lang/file-discovery";
import type { ExtractedHit } from "./lang/types";
import { shouldEmit } from "./rules";
import {
DittoScanDetectionKindSchema,
type DittoScanCandidate,
type DittoScanDetectionKind,
} from "./types";
import { createHash } from "crypto";
import type { FileDiscoveryStats } from "./lang/file-discovery";
import { shouldEmit } from "./rules";
import { walkCodebase } from "./walk";

export interface DittoScanExtractOptions {
Expand Down Expand Up @@ -231,6 +232,29 @@ export function makeCandidateId(
.slice(0, 12);
}

export function assignOccurrenceIndexes(
hits: readonly Pick<ExtractedHit, "value" | "location">[]
): number[] {
const sourceOrder = hits
.map((_, index) => index)
.sort((a, b) => {
const left = hits[a].location;
const right = hits[b].location;
return left.line - right.line || left.column - right.column || a - b;
});

const seen = new Map<string, number>();
const indexes = new Array<number>(hits.length);

for (const index of sourceOrder) {
const count = seen.get(hits[index].value) ?? 0;
indexes[index] = count;
seen.set(hits[index].value, count + 1);
}

return indexes;
}

export async function runExtract(
opts: DittoScanExtractOptions
): Promise<DittoScanExtractResult> {
Expand Down Expand Up @@ -266,8 +290,10 @@ export async function runExtract(

const lines = file.source.split(/\r?\n/);

for (const hit of hits) {
if (!shouldEmit(hit.value, hit.context)) continue;
const emitted = hits.filter((hit) => shouldEmit(hit.value, hit.context));
const occurrenceIndexes = assignOccurrenceIndexes(emitted);

for (const [index, hit] of emitted.entries()) {
const candidate: DittoScanCandidate = {
id: makeCandidateId(
file.relPath,
Expand All @@ -282,6 +308,7 @@ export async function runExtract(
line: hit.location.line,
column: hit.location.column,
},
occurrence_index: occurrenceIndexes[index],
language: file.languageLabel,
locale_key: hit.localeKey ?? file.localeKey,
i18n_key: hit.i18nKey ?? null,
Expand Down
1 change: 1 addition & 0 deletions lib/src/scan/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const DittoScanCandidateSchema = z.object({
line: z.number().int().positive(),
column: z.number().int().positive(),
}),
occurrence_index: z.number().int().nonnegative(),
language: z.string(),
// Locale key derived from the file's path when the candidate comes from a
// per-locale i18n resource file admitted by i18n file discovery (e.g. "en"
Expand Down
Loading