Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/quiet-otters-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nodesecure/js-x-ray": minor
---

feat(tracer): resolve identifiers assigned an object literal, so `log-usage` detects `pino()`/`winston.createLogger()` config passed via a variable instead of only inline
13 changes: 13 additions & 0 deletions workspaces/js-x-ray/src/VariableTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ export class VariableTracer extends EventEmitter {

// PUBLIC PROPERTIES
literalIdentifiers = new Map<string, LiteralIdentifier>();
/**
* Resolves an identifier assigned an object literal back to its ObjectExpression node.
* @example
* const opts = { useOnlyCustomLevels: true };
* pino(opts); // "opts" resolves via objectIdentifiers
*/
objectIdentifiers = new Map<string, ESTree.ObjectExpression>();
importedModules = new Set<string>();

// PRIVATE PROPERTIES
Expand Down Expand Up @@ -494,6 +501,12 @@ export class VariableTracer extends EventEmitter {
* ^ ObjectExpression
*/
case "ObjectExpression": {
// Only record top-level assignments (`const x = {...}`) so consumers
// can resolve "x" back to its object shape, e.g. `pino(x)`.
if (childNode === variableDeclaratorNode.init) {
this.objectIdentifiers.set(id.name, childNode);
}

for (const property of childNode.properties) {
let node: ESTree.Node | null = null;
if (property.type === "Property") {
Expand Down
10 changes: 8 additions & 2 deletions workspaces/js-x-ray/src/probes/log-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ function createWinstonCreateLoggerTracerListener(tracer: VariableTracer, logUsag
let winstonLoggerMethods = winstonCreateLoggerChildLoggerFunctions.get(payload.name) ?? [...kWinstonLogMethods];

winston: if (payload.name === "winston.createLogger") {
const winstonContext = payload.arguments[0];
const winstonContextArg = payload.arguments[0];
const winstonContext = winstonContextArg && isIdentifier(winstonContextArg) ?
tracer.objectIdentifiers.get(winstonContextArg.name) :
winstonContextArg;
if (!winstonContext || winstonContext.type !== "ObjectExpression") {
break winston;
}
Expand Down Expand Up @@ -174,7 +177,10 @@ function createPinoTracerListener(tracer: VariableTracer, logUsages: Set<string>
let pinoLoggerMethods: string[] = pinoLoggerChildLoggerFunctions.get(payload.name) ?? [...kPinoLogMethods];

pino: if (payload.name === "pino") {
const pinoContext = payload.arguments[0];
const pinoContextArg = payload.arguments[0];
const pinoContext = pinoContextArg && isIdentifier(pinoContextArg) ?
tracer.objectIdentifiers.get(pinoContextArg.name) :
pinoContextArg;
if (!pinoContext || pinoContext.type !== "ObjectExpression") {
break pino;
}
Expand Down
23 changes: 23 additions & 0 deletions workspaces/js-x-ray/test/VariableTracer/assignments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,29 @@ test("it should be able to Trace template literals who has being assigned", () =
});
});

test("it should be able to resolve an identifier assigned an object literal", () => {
const helpers = createTracer();

helpers.walkOnCode(`
const opts = { useOnlyCustomLevels: true, foo: "bar" };
`);
assert.ok(helpers.tracer.objectIdentifiers.has("opts"));

const objectNode = helpers.tracer.objectIdentifiers.get("opts")!;
assert.strictEqual(objectNode.type, "ObjectExpression");
assert.strictEqual(objectNode.properties.length, 2);
});

test("it should not resolve a non top-level object literal as an identifier", () => {
const helpers = createTracer();

helpers.walkOnCode(`
const opts = { nested: { useOnlyCustomLevels: true } };
`);
assert.ok(helpers.tracer.objectIdentifiers.has("opts"));
assert.strictEqual(helpers.tracer.objectIdentifiers.has("nested"), false);
});

test("it should be able to Trace a global assignment using a LogicalExpression", () => {
const helpers = createTracer(true);
const assignments = helpers.getAssignmentArray();
Expand Down
56 changes: 56 additions & 0 deletions workspaces/js-x-ray/test/probes/log-usage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,33 @@ describe("log-usage probe", () => {
assert.strictEqual(firstWarning.value, "logger.foo, logger.bar");
});

it("should resolve the whole pino() config object when passed as an identifier", () => {
const code = `import pino from "pino";

const opts = {
customLevels:{
foo: 35,
bar: 36
},
useOnlyCustomLevels: true,
};
const logger = pino(opts);
logger.info("hello");
logger.warn("hello");
logger.foo("hello");
logger.bar("hello");
`;
const { warnings } = new AstAnalyser({
optionalWarnings: true
}).analyse(code);

const [firstWarning] = warnings;

assert.strictEqual(firstWarning.kind, "log-usage");
assert.strictEqual(firstWarning.severity, "Information");
assert.strictEqual(firstWarning.value, "logger.foo, logger.bar");
});

it("should trace default methods as well when useOnlyCustomLevels is false", () => {
const code = `import pino from "pino";
const logger = pino({
Expand Down Expand Up @@ -876,6 +903,35 @@ describe("log-usage probe", () => {
assert.strictEqual(firstWarning.value, "logger.foo, logger.bar");
});

it("should resolve the whole winston.createLogger() config object when passed as an identifier", () => {
const code = `const winston = require("winston");
const opts = {
format: winston.format.json(),
transports: [new winston.transports.Console()],
levels: {
foo: 1,
bar: 2
}
};
const logger = winston.createLogger(opts);

logger.info("hello");
logger.warn("hello");
logger.foo("hello");
logger.bar("hello");
`;

const { warnings } = new AstAnalyser({
optionalWarnings: true
}).analyse(code);

const [firstWarning] = warnings;

assert.strictEqual(firstWarning.kind, "log-usage");
assert.strictEqual(firstWarning.severity, "Information");
assert.strictEqual(firstWarning.value, "logger.foo, logger.bar");
});

it("should have a child logger who inherit the custom levels from its parent logger", () => {
const code = `import winston from "winston";
const logger = winston.createLogger({
Expand Down