Skip to content
Merged
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
2 changes: 1 addition & 1 deletion dist/tree-sitter.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions dist/tree-sitter.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/tree-sitter.js.map

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions src/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,17 @@ const JS_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".cjs"]);
/** Max length for extractable scalar constant values. */
const MAX_CONSTANT_VALUE_LENGTH = 128;

/**
* Secret-like name suffix pattern — constants whose names end with these
* segments (after `_` or as the full name) are skipped to prevent accidental
* credential storage in the knowledge graph. Defense-in-depth measure.
*/
const SECRET_NAME_SUFFIX = /(?:^|_)(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)$/i;

/**
* Extract top-level const declarations from the program root.
* Only captures constants at module scope — not inside functions.
* Skips constants whose names match secret-like patterns (defense-in-depth).
*/
function extractConstants(rootNode: SyntaxNode): ParsedConstant[] {
const constants: ParsedConstant[] = [];
Expand All @@ -68,6 +76,9 @@ function extractConstants(rootNode: SyntaxNode): ParsedConstant[] {
const nameNode = declarator.childForFieldName("name");
if (!nameNode || nameNode.type !== "identifier") continue;

// Skip constants with secret-like names (defense-in-depth)
if (SECRET_NAME_SUFFIX.test(nameNode.text)) continue;

const valueNode = declarator.childForFieldName("value");

// Skip arrow functions — already handled as ParsedFunction
Expand Down
57 changes: 53 additions & 4 deletions tests/design-system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,16 +268,14 @@ describe("constant extraction", () => {
expect(colors!.isExported).toBe(true);
});

it("marks non-exported constants correctly", () => {
it("skips constants with secret-like names (INTERNAL_KEY)", () => {
const result = parseFile(
path.join(FIXTURES_DIR, "src/constants.ts"),
"src/constants.ts",
);

const internal = result.constants?.find((c) => c.name === "INTERNAL_KEY");
expect(internal).toBeDefined();
expect(internal!.value).toBe("secret-key-123");
expect(internal!.isExported).toBe(false);
expect(internal).toBeUndefined();
});

it("extracts type annotation when present", () => {
Expand Down Expand Up @@ -410,6 +408,57 @@ describe("constant extraction", () => {
expect(another).toBeUndefined();
});

it("skips exported constants with secret-like names", () => {
const result = parseFile(
path.join(FIXTURES_DIR, "src/constants.ts"),
"src/constants.ts",
);

// All of these should be skipped — names contain secret-like patterns
expect(
result.constants?.find((c) => c.name === "DATABASE_PASSWORD"),
).toBeUndefined();
expect(
result.constants?.find((c) => c.name === "STRIPE_SECRET_KEY"),
).toBeUndefined();
expect(
result.constants?.find((c) => c.name === "AWS_ACCESS_KEY"),
).toBeUndefined();
expect(
result.constants?.find((c) => c.name === "AUTH_TOKEN"),
).toBeUndefined();
expect(result.constants?.find((c) => c.name === "API_KEY")).toBeUndefined();
expect(
result.constants?.find((c) => c.name === "JWT_CREDENTIAL"),
).toBeUndefined();
});

it("keeps constants whose names contain partial secret-like substrings in non-secret context", () => {
const result = parseFile(
path.join(FIXTURES_DIR, "src/constants.ts"),
"src/constants.ts",
);

// KEYBOARD_SHORTCUT contains "KEY" but is not a secret — word boundary matters
const keyboard = result.constants?.find(
(c) => c.name === "KEYBOARD_SHORTCUT",
);
expect(keyboard).toBeDefined();
expect(keyboard!.value).toBe("Ctrl+K");

// TOKEN_LIMIT contains "TOKEN" but is a numeric limit, not a secret
const tokenLimit = result.constants?.find((c) => c.name === "TOKEN_LIMIT");
expect(tokenLimit).toBeDefined();
expect(tokenLimit!.value).toBe("4096");

// SECRET_SAUCE_RECIPE contains "SECRET" but is not a credential
const sauce = result.constants?.find(
(c) => c.name === "SECRET_SAUCE_RECIPE",
);
expect(sauce).toBeDefined();
expect(sauce!.value).toBe("tomato");
});

it("omits constants field for files with no constants", () => {
const result = parseFile(
path.join(FIXTURES_DIR, "src/components.tsx"),
Expand Down
12 changes: 12 additions & 0 deletions tests/fixtures/design-system/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,15 @@ export const SIZES = ["sm", "md", "lg"] as const;
let mutableVal = "x",
anotherVal = "y";
export { mutableVal, anotherVal };

// Secret-named constants — should be skipped by parser (defense-in-depth)
export const DATABASE_PASSWORD = "hunter2";
export const STRIPE_SECRET_KEY = "sk_test_abc123";
const AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE";
export const AUTH_TOKEN = "eyJhbGciOiJIUzI1NiJ9";
export const API_KEY = "pk_live_xxx";
export const JWT_CREDENTIAL = "my-credential-value";
// Non-secret constants that happen to contain partial matches should be KEPT
export const KEYBOARD_SHORTCUT = "Ctrl+K";
export const TOKEN_LIMIT = 4096;
export const SECRET_SAUCE_RECIPE = "tomato";