Skip to content
Draft
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
68 changes: 68 additions & 0 deletions src/core/envVarAnalysis/accessorConstructors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expect, it } from "bun:test";

import { analyse } from "./analysis.testUtils.js";

const flow = "src/flows/a.flow.ts";
const reader = `class Reader {
field = process.env.FIELD;
constructor(key: string) { console.log(process.env[key]); }
}`;

describe("constructor accessor keys", () => {
it.each([
'export default () => new Reader("TOKEN");',
'function read(key: string) { return new Reader(key); } export default () => read("TOKEN");',
'class Derived extends Reader {} export default () => new Derived("TOKEN");',
`class Derived extends Reader {
constructor(key: string) { super(key); }
} export default () => new Derived("TOKEN");`,
])("resolves constructor keys with instance reads: %s", async (source) => {
const result = await analyse({ [flow]: `${reader} ${source}` });
try {
expect(result.byFlow.get(flow)).toEqual({
names: ["FIELD", "TOKEN"],
mayBeIncomplete: false,
});
} finally {
await result.cleanup();
}
});

it("forwards keys to an overloaded constructor", async () => {
const result = await analyse({
[flow]: `class Reader {
field = process.env.FIELD;
constructor(key: string);
constructor(key: string) { console.log(process.env[key]); }
}
function read(key: string) { return new Reader(key); }
export default () => read("TOKEN");`,
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: ["FIELD", "TOKEN"],
mayBeIncomplete: false,
});
} finally {
await result.cleanup();
}
});

it("resolves every constructor key slot", async () => {
const result = await analyse({
[flow]: `class Reader {
constructor(first: string, second: string) {
console.log(process.env[first], process.env[second]);
}
} export default () => new Reader("FIRST", "SECOND");`,
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: ["FIRST", "SECOND"],
mayBeIncomplete: false,
});
} finally {
await result.cleanup();
}
});
});
67 changes: 67 additions & 0 deletions src/core/envVarAnalysis/accessorKeys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from "bun:test";

import { analyse } from "./analysis.testUtils.js";

const flow = "src/flows/a.flow.ts";

describe("accessor keys", () => {
it.each([
`function read(first: string, second: string) {
return process.env[first] + process.env[second];
}`,
`function env(key: string) { return process.env[key]; }
function read(first: string, second: string) {
return env(first) + env(second);
}`,
`function read(first: string, second: string): unknown {
return next(first, second);
}
function next(first: string, second: string): unknown {
return process.env[first] ?? process.env[second] ?? read(first, second);
}`,
])("resolves every key slot through calls: %s", async (helper) => {
const result = await analyse({
[flow]: `${helper} export default () => read("FIRST", "SECOND");`,
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: ["FIRST", "SECOND"],
mayBeIncomplete: false,
});
} finally {
await result.cleanup();
}
});

it("accepts literal names containing template markers", async () => {
const result = await analyse({
[flow]:
"function env(key: string) { return process.env[key]; }\n" +
'export default () => [env("TOKEN${SUFFIX}"), env(`LITERAL\\${KEY}`)];',
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: ["LITERAL${KEY}", "TOKEN${SUFFIX}"],
mayBeIncomplete: false,
});
} finally {
await result.cleanup();
}
});

it("keeps interpolated key expressions incomplete", async () => {
const result = await analyse({
[flow]:
"function env(key: string) { return process.env[key]; }\n" +
"export default (suffix: string) => env(`TOKEN${suffix}`);",
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: [],
mayBeIncomplete: true,
});
} finally {
await result.cleanup();
}
});
});
79 changes: 79 additions & 0 deletions src/core/envVarAnalysis/accessorScopes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expect, it } from "bun:test";

import { analyse } from "./analysis.testUtils.js";

const flow = "src/flows/a.flow.ts";

describe("accessor scopes", () => {
it("resolves reverse-order forwarding chains without a depth limit", async () => {
const helpers =
Array.from(
{ length: 20 },
(_, index) =>
`export function read${index}(name: string) { return read${index + 1}(name); }`,
).join("\n") +
`\nexport function read20(name: string) { return process.env[name]; }`;
const result = await analyse({
"src/lib/env.ts": helpers,
[flow]: `import { read0 } from "../lib/env.js"; export default () => read0("TOKEN");`,
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: ["TOKEN"],
mayBeIncomplete: false,
});
expect(result.accessorCount).toBe(21);
} finally {
await result.cleanup();
}
});

it("resolves a literal passed to an accessor in the flow file", async () => {
const result = await analyse({
[flow]: `function env(name: string) { return process.env[name]; }
export default () => env("TOKEN");`,
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: ["TOKEN"],
mayBeIncomplete: false,
});
} finally {
await result.cleanup();
}
});

it("does not mistake a shadowed parameter for an accessor key", async () => {
const result = await analyse({
[flow]: `function env(key: string) {
{ const key = "ACTUAL"; return process.env[key]; }
}
export default () => env("FALSE_POSITIVE");`,
});
try {
expect(result.byFlow.get(flow)?.names).not.toContain("FALSE_POSITIVE");
expect(result.byFlow.get(flow)?.mayBeIncomplete).toBe(true);
expect(result.accessorCount).toBe(0);
} finally {
await result.cleanup();
}
});

it("does not classify a helper by reads inside a dormant nested function", async () => {
const result = await analyse({
[flow]: `function helper(name: string) {
function later(name: string) { return process.env[name]; }
return process.env.USED;
}
export default () => helper("NEVER");`,
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: ["USED"],
mayBeIncomplete: false,
});
} finally {
await result.cleanup();
}
});
});
85 changes: 85 additions & 0 deletions src/core/envVarAnalysis/accessorWrites.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from "bun:test";

import { analyse } from "./analysis.testUtils.js";

const flow = "src/flows/a.flow.ts";

describe("accessor writes", () => {
it.each([
'key = "OTHER";',
'key += "_OTHER";',
"++key;",
"key++;",
'[key] = ["OTHER"];',
'({ key } = { key: "OTHER" });',
'for (key of ["OTHER"]) {}',
"for (key in { OTHER: true }) {}",
'function change() { key = "OTHER"; } change();',
])("does not infer a key after a write: %s", async (write) => {
const result = await analyse({
[flow]: `function env(key: string) { ${write} return process.env[key]; }
export default () => env("INPUT");`,
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: [],
mayBeIncomplete: true,
});
expect(result.accessorCount).toBe(0);
} finally {
await result.cleanup();
}
});

it("does not forward a reassigned key", async () => {
const result = await analyse({
[flow]: `function env(key: string) { return process.env[key]; }
function read(key: string) { key = "OTHER"; return env(key); }
export default () => read("INPUT");`,
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: [],
mayBeIncomplete: true,
});
} finally {
await result.cleanup();
}
});

it("retains untouched key slots beside a reassigned slot", async () => {
const result = await analyse({
[flow]: `function env(first: string, second: string) {
first = "OTHER";
return process.env[first] + process.env[second];
}
export default () => env("INPUT", "SECOND");`,
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: ["SECOND"],
mayBeIncomplete: true,
});
} finally {
await result.cleanup();
}
});

it("does not invalidate a parameter for a shadowed write", async () => {
const result = await analyse({
[flow]: `function env(key: string) {
{ let key = "LOCAL"; key = "OTHER"; }
return process.env[key];
}
export default () => env("INPUT");`,
});
try {
expect(result.byFlow.get(flow)).toEqual({
names: ["INPUT"],
mayBeIncomplete: false,
});
} finally {
await result.cleanup();
}
});
});
Loading
Loading