@logtape/lint 2.2.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/dist/core/ast.cjs +517 -0
- package/dist/core/ast.js +506 -0
- package/dist/core/ast.js.map +1 -0
- package/dist/deno/plugin.cjs +332 -0
- package/dist/deno/plugin.d.cts +46 -0
- package/dist/deno/plugin.d.cts.map +1 -0
- package/dist/deno/plugin.d.ts +46 -0
- package/dist/deno/plugin.d.ts.map +1 -0
- package/dist/deno/plugin.js +333 -0
- package/dist/deno/plugin.js.map +1 -0
- package/dist/eslint/plugin.cjs +51 -0
- package/dist/eslint/plugin.d.cts +26 -0
- package/dist/eslint/plugin.d.cts.map +1 -0
- package/dist/eslint/plugin.d.ts +26 -0
- package/dist/eslint/plugin.d.ts.map +1 -0
- package/dist/eslint/plugin.js +44 -0
- package/dist/eslint/plugin.js.map +1 -0
- package/dist/mod.cjs +15 -0
- package/dist/mod.d.cts +6 -0
- package/dist/mod.d.ts +6 -0
- package/dist/mod.js +7 -0
- package/dist/rules/no-message-interpolation.cjs +52 -0
- package/dist/rules/no-message-interpolation.d.cts +23 -0
- package/dist/rules/no-message-interpolation.d.cts.map +1 -0
- package/dist/rules/no-message-interpolation.d.ts +23 -0
- package/dist/rules/no-message-interpolation.d.ts.map +1 -0
- package/dist/rules/no-message-interpolation.js +53 -0
- package/dist/rules/no-message-interpolation.js.map +1 -0
- package/dist/rules/no-unawaited-log.cjs +67 -0
- package/dist/rules/no-unawaited-log.d.cts +21 -0
- package/dist/rules/no-unawaited-log.d.cts.map +1 -0
- package/dist/rules/no-unawaited-log.d.ts +21 -0
- package/dist/rules/no-unawaited-log.d.ts.map +1 -0
- package/dist/rules/no-unawaited-log.js +68 -0
- package/dist/rules/no-unawaited-log.js.map +1 -0
- package/dist/rules/prefer-lazy-evaluation.cjs +59 -0
- package/dist/rules/prefer-lazy-evaluation.d.cts +22 -0
- package/dist/rules/prefer-lazy-evaluation.d.cts.map +1 -0
- package/dist/rules/prefer-lazy-evaluation.d.ts +22 -0
- package/dist/rules/prefer-lazy-evaluation.d.ts.map +1 -0
- package/dist/rules/prefer-lazy-evaluation.js +60 -0
- package/dist/rules/prefer-lazy-evaluation.js.map +1 -0
- package/dist/rules/require-meta-sink.cjs +75 -0
- package/dist/rules/require-meta-sink.d.cts +27 -0
- package/dist/rules/require-meta-sink.d.cts.map +1 -0
- package/dist/rules/require-meta-sink.d.ts +27 -0
- package/dist/rules/require-meta-sink.d.ts.map +1 -0
- package/dist/rules/require-meta-sink.js +76 -0
- package/dist/rules/require-meta-sink.js.map +1 -0
- package/dist/utils.cjs +82 -0
- package/dist/utils.js +82 -0
- package/dist/utils.js.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-meta-sink.d.cts","names":[],"sources":["../../src/rules/require-meta-sink.ts"],"sourcesContent":[],"mappings":";;;;;;AAyBA;;;;;;;;;;;;;;;;cAAa,iBAAiB,IAAA,CAAK"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Rule } from "eslint";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/require-meta-sink.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ESLint rule that detects `configure()` / `configureSync()` calls imported
|
|
7
|
+
* from `@logtape/logtape` that lack a dedicated sink for the meta logger.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* // Incorrect (missing meta sink):
|
|
11
|
+
* await configure({ sinks: { main: s }, loggers: [{ category: [], sinks: ["main"] }] });
|
|
12
|
+
*
|
|
13
|
+
* // Correct:
|
|
14
|
+
* await configure({
|
|
15
|
+
* sinks: { console: getConsoleSink(), main: s },
|
|
16
|
+
* loggers: [
|
|
17
|
+
* { category: ["logtape", "meta"], sinks: ["console"] },
|
|
18
|
+
* { category: [], sinks: ["main"] },
|
|
19
|
+
* ],
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare const requireMetaSink: Rule.RuleModule;
|
|
24
|
+
//# sourceMappingURL=require-meta-sink.d.ts.map
|
|
25
|
+
//#endregion
|
|
26
|
+
export { requireMetaSink };
|
|
27
|
+
//# sourceMappingURL=require-meta-sink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-meta-sink.d.ts","names":[],"sources":["../../src/rules/require-meta-sink.ts"],"sourcesContent":[],"mappings":";;;;;;AAyBA;;;;;;;;;;;;;;;;cAAa,iBAAiB,IAAA,CAAK"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { configNeedsMetaSink, isLogtapeImportSource, unwrapTypeAssertion } from "../core/ast.js";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/require-meta-sink.ts
|
|
4
|
+
/**
|
|
5
|
+
* ESLint rule that detects `configure()` / `configureSync()` calls imported
|
|
6
|
+
* from `@logtape/logtape` that lack a dedicated sink for the meta logger.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // Incorrect (missing meta sink):
|
|
10
|
+
* await configure({ sinks: { main: s }, loggers: [{ category: [], sinks: ["main"] }] });
|
|
11
|
+
*
|
|
12
|
+
* // Correct:
|
|
13
|
+
* await configure({
|
|
14
|
+
* sinks: { console: getConsoleSink(), main: s },
|
|
15
|
+
* loggers: [
|
|
16
|
+
* { category: ["logtape", "meta"], sinks: ["console"] },
|
|
17
|
+
* { category: [], sinks: ["main"] },
|
|
18
|
+
* ],
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
const requireMetaSink = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: "suggestion",
|
|
25
|
+
docs: {
|
|
26
|
+
description: "Require a dedicated sink for the LogTape meta logger in configure() calls",
|
|
27
|
+
recommended: true,
|
|
28
|
+
url: "https://logtape.org/lint/require-meta-sink"
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
messages: { requireMeta: "Add a dedicated sink for the meta logger (category: [\"logtape\"] or [\"logtape\", \"meta\"]) to handle LogTape's own diagnostic messages." }
|
|
32
|
+
},
|
|
33
|
+
create(context) {
|
|
34
|
+
const configFns = /* @__PURE__ */ new Set();
|
|
35
|
+
return {
|
|
36
|
+
ImportDeclaration(node) {
|
|
37
|
+
if (!isLogtapeImportSource(node.source?.value)) return;
|
|
38
|
+
for (const specifier of node.specifiers ?? []) {
|
|
39
|
+
if (specifier.type !== "ImportSpecifier") continue;
|
|
40
|
+
const imported = specifier.imported?.name;
|
|
41
|
+
if (imported === "configure" || imported === "configureSync") configFns.add(specifier.local?.name);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
CallExpression(node) {
|
|
45
|
+
if (configFns.size === 0) return;
|
|
46
|
+
const { callee } = node;
|
|
47
|
+
if (callee.type !== "Identifier") return;
|
|
48
|
+
const calleeName = callee.name;
|
|
49
|
+
if (!configFns.has(calleeName)) return;
|
|
50
|
+
const scope = context.sourceCode?.getScope?.(node) ?? context.getScope?.();
|
|
51
|
+
if (scope) {
|
|
52
|
+
let cur = scope;
|
|
53
|
+
let calleeVar = null;
|
|
54
|
+
while (cur) {
|
|
55
|
+
const v = cur.set?.get(calleeName);
|
|
56
|
+
if (v) {
|
|
57
|
+
calleeVar = v;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
cur = cur.upper;
|
|
61
|
+
}
|
|
62
|
+
if (!calleeVar?.defs?.some((d) => d.type === "ImportBinding")) return;
|
|
63
|
+
}
|
|
64
|
+
const configArg = unwrapTypeAssertion(node.arguments[0]);
|
|
65
|
+
if (configNeedsMetaSink(configArg)) context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: "requireMeta"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { requireMetaSink };
|
|
76
|
+
//# sourceMappingURL=require-meta-sink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-meta-sink.js","names":["requireMetaSink: Rule.RuleModule","cur: any","calleeVar: any","d: any"],"sources":["../../src/rules/require-meta-sink.ts"],"sourcesContent":["import type { Rule } from \"eslint\";\nimport {\n configNeedsMetaSink,\n isLogtapeImportSource,\n unwrapTypeAssertion,\n} from \"../core/ast.ts\";\n\n/**\n * ESLint rule that detects `configure()` / `configureSync()` calls imported\n * from `@logtape/logtape` that lack a dedicated sink for the meta logger.\n *\n * ```ts\n * // Incorrect (missing meta sink):\n * await configure({ sinks: { main: s }, loggers: [{ category: [], sinks: [\"main\"] }] });\n *\n * // Correct:\n * await configure({\n * sinks: { console: getConsoleSink(), main: s },\n * loggers: [\n * { category: [\"logtape\", \"meta\"], sinks: [\"console\"] },\n * { category: [], sinks: [\"main\"] },\n * ],\n * });\n * ```\n */\nexport const requireMetaSink: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"Require a dedicated sink for the LogTape meta logger in configure() calls\",\n recommended: true,\n url: \"https://logtape.org/lint/require-meta-sink\",\n },\n schema: [],\n messages: {\n requireMeta: \"Add a dedicated sink for the meta logger \" +\n '(category: [\"logtape\"] or [\"logtape\", \"meta\"]) ' +\n \"to handle LogTape's own diagnostic messages.\",\n },\n },\n\n create(context) {\n // Track local names of configure/configureSync imported from @logtape/logtape\n const configFns = new Set<string>();\n\n return {\n ImportDeclaration(node) {\n // deno-lint-ignore no-explicit-any\n if (!isLogtapeImportSource((node as any).source?.value)) return;\n // deno-lint-ignore no-explicit-any\n for (const specifier of (node as any).specifiers ?? []) {\n if (specifier.type !== \"ImportSpecifier\") continue;\n const imported = specifier.imported?.name;\n if (imported === \"configure\" || imported === \"configureSync\") {\n configFns.add(specifier.local?.name);\n }\n }\n },\n\n CallExpression(node) {\n if (configFns.size === 0) return;\n\n const { callee } = node;\n if (callee.type !== \"Identifier\") return;\n // deno-lint-ignore no-explicit-any\n const calleeName = (callee as any).name as string;\n if (!configFns.has(calleeName)) return;\n\n // Verify the callee resolves to the actual import, not a shadow\n // (e.g. a function parameter also named configure).\n // deno-lint-ignore no-explicit-any\n const scope = (context as any).sourceCode?.getScope?.(node) ??\n // deno-lint-ignore no-explicit-any\n (context as any).getScope?.();\n if (scope) {\n // deno-lint-ignore no-explicit-any\n let cur: any = scope;\n // deno-lint-ignore no-explicit-any\n let calleeVar: any = null;\n while (cur) {\n const v = cur.set?.get(calleeName);\n if (v) {\n calleeVar = v;\n break;\n }\n cur = cur.upper;\n }\n if (\n // deno-lint-ignore no-explicit-any\n !calleeVar?.defs?.some((d: any) => d.type === \"ImportBinding\")\n ) return;\n }\n\n // Unwrap a TypeScript type assertion (e.g. `configure({ ... } as const)`).\n const configArg = unwrapTypeAssertion(node.arguments[0]);\n if (configNeedsMetaSink(configArg)) {\n context.report({ node, messageId: \"requireMeta\" });\n }\n },\n };\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyBA,MAAaA,kBAAmC;CAC9C,MAAM;EACJ,MAAM;EACN,MAAM;GACJ,aACE;GACF,aAAa;GACb,KAAK;EACN;EACD,QAAQ,CAAE;EACV,UAAU,EACR,aAAa,6IAGd;CACF;CAED,OAAO,SAAS;EAEd,MAAM,4BAAY,IAAI;AAEtB,SAAO;GACL,kBAAkB,MAAM;AAEtB,SAAK,sBAAuB,KAAa,QAAQ,MAAM,CAAE;AAEzD,SAAK,MAAM,aAAc,KAAa,cAAc,CAAE,GAAE;AACtD,SAAI,UAAU,SAAS,kBAAmB;KAC1C,MAAM,WAAW,UAAU,UAAU;AACrC,SAAI,aAAa,eAAe,aAAa,gBAC3C,WAAU,IAAI,UAAU,OAAO,KAAK;IAEvC;GACF;GAED,eAAe,MAAM;AACnB,QAAI,UAAU,SAAS,EAAG;IAE1B,MAAM,EAAE,QAAQ,GAAG;AACnB,QAAI,OAAO,SAAS,aAAc;IAElC,MAAM,aAAc,OAAe;AACnC,SAAK,UAAU,IAAI,WAAW,CAAE;IAKhC,MAAM,QAAQ,AAAC,QAAgB,YAAY,WAAW,KAAK,IAEzD,AAAC,QAAgB,YAAY;AAC/B,QAAI,OAAO;KAET,IAAIC,MAAW;KAEf,IAAIC,YAAiB;AACrB,YAAO,KAAK;MACV,MAAM,IAAI,IAAI,KAAK,IAAI,WAAW;AAClC,UAAI,GAAG;AACL,mBAAY;AACZ;MACD;AACD,YAAM,IAAI;KACX;AACD,UAEG,WAAW,MAAM,KAAK,CAACC,MAAW,EAAE,SAAS,gBAAgB,CAC9D;IACH;IAGD,MAAM,YAAY,oBAAoB,KAAK,UAAU,GAAG;AACxD,QAAI,oBAAoB,UAAU,CAChC,SAAQ,OAAO;KAAE;KAAM,WAAW;IAAe,EAAC;GAErD;EACF;CACF;AACF"}
|
package/dist/utils.cjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const require_ast = require('./core/ast.cjs');
|
|
2
|
+
|
|
3
|
+
//#region src/utils.ts
|
|
4
|
+
/**
|
|
5
|
+
* Returns visitor hooks and a scope-aware checker for detecting LogTape log
|
|
6
|
+
* method calls. Uses ESLint's scope manager to resolve variable bindings,
|
|
7
|
+
* so parameter- and local-variable shadowing are handled correctly.
|
|
8
|
+
*
|
|
9
|
+
* Wire the returned `ImportDeclaration` hook into your rule's visitor object,
|
|
10
|
+
* then call `isLogtapeCall(node.callee, node)` inside `CallExpression`.
|
|
11
|
+
*/
|
|
12
|
+
function createLogtapeScope(context) {
|
|
13
|
+
const getterNames = /* @__PURE__ */ new Set();
|
|
14
|
+
const lazyNames = /* @__PURE__ */ new Set();
|
|
15
|
+
function resolveVariable(scope, name) {
|
|
16
|
+
let current = scope;
|
|
17
|
+
while (current) {
|
|
18
|
+
const variable = current.set?.get(name);
|
|
19
|
+
if (variable) return variable;
|
|
20
|
+
current = current.upper;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function resolvesToImportedGetter(name, scope) {
|
|
25
|
+
if (!getterNames.has(name)) return false;
|
|
26
|
+
const calleeVar = resolveVariable(scope, name);
|
|
27
|
+
return calleeVar?.defs?.some((d) => d.type === "ImportBinding") ?? false;
|
|
28
|
+
}
|
|
29
|
+
function isLoggerExpression(node, scope, depth = 0) {
|
|
30
|
+
if (depth > 16 || !node) return false;
|
|
31
|
+
if (node.type === "CallExpression" && node.callee?.type === "Identifier") return resolvesToImportedGetter(node.callee.name, scope);
|
|
32
|
+
if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && node.callee.property?.type === "Identifier" && (node.callee.property.name === "with" || node.callee.property.name === "getChild")) return isLoggerExpression(node.callee.object, scope, depth + 1);
|
|
33
|
+
if (node.type === "Identifier") {
|
|
34
|
+
const variable = resolveVariable(scope, node.name);
|
|
35
|
+
if (!variable) return false;
|
|
36
|
+
return variable.defs?.some((def) => {
|
|
37
|
+
if (def.type !== "Variable") return false;
|
|
38
|
+
return isLoggerExpression(def.node?.init, variable.scope ?? scope, depth + 1);
|
|
39
|
+
}) ?? false;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function isLogtapeCall(callee, callNode) {
|
|
44
|
+
if (!callee || callee.type !== "MemberExpression") return false;
|
|
45
|
+
const scope = context.sourceCode?.getScope?.(callNode) ?? context.getScope?.();
|
|
46
|
+
return isLoggerExpression(callee.object, scope);
|
|
47
|
+
}
|
|
48
|
+
function effectiveLazyNames(callNode) {
|
|
49
|
+
if (lazyNames.size === 0) return lazyNames;
|
|
50
|
+
const scope = context.sourceCode?.getScope?.(callNode) ?? context.getScope?.();
|
|
51
|
+
const result = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const name of lazyNames) {
|
|
53
|
+
const variable = resolveVariable(scope, name);
|
|
54
|
+
const isImport = variable?.defs?.some((d) => d.type === "ImportBinding") ?? false;
|
|
55
|
+
if (isImport) result.add(name);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
ImportDeclaration(node) {
|
|
61
|
+
if (!require_ast.isLogtapeImportSource(node.source?.value)) return;
|
|
62
|
+
for (const spec of node.specifiers ?? []) {
|
|
63
|
+
if (spec.type !== "ImportSpecifier") continue;
|
|
64
|
+
if (spec.imported?.name === "getLogger") getterNames.add(spec.local?.name);
|
|
65
|
+
else if (spec.imported?.name === "lazy") lazyNames.add(spec.local?.name);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
isLogtapeCall,
|
|
69
|
+
lazyNames,
|
|
70
|
+
effectiveLazyNames
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the SourceCode object from a rule context (v8/v9 compatible).
|
|
75
|
+
*/
|
|
76
|
+
function getSourceCode(context) {
|
|
77
|
+
return context.sourceCode ?? context.getSourceCode?.();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
exports.createLogtapeScope = createLogtapeScope;
|
|
82
|
+
exports.getSourceCode = getSourceCode;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isLogtapeImportSource } from "./core/ast.js";
|
|
2
|
+
|
|
3
|
+
//#region src/utils.ts
|
|
4
|
+
/**
|
|
5
|
+
* Returns visitor hooks and a scope-aware checker for detecting LogTape log
|
|
6
|
+
* method calls. Uses ESLint's scope manager to resolve variable bindings,
|
|
7
|
+
* so parameter- and local-variable shadowing are handled correctly.
|
|
8
|
+
*
|
|
9
|
+
* Wire the returned `ImportDeclaration` hook into your rule's visitor object,
|
|
10
|
+
* then call `isLogtapeCall(node.callee, node)` inside `CallExpression`.
|
|
11
|
+
*/
|
|
12
|
+
function createLogtapeScope(context) {
|
|
13
|
+
const getterNames = /* @__PURE__ */ new Set();
|
|
14
|
+
const lazyNames = /* @__PURE__ */ new Set();
|
|
15
|
+
function resolveVariable(scope, name) {
|
|
16
|
+
let current = scope;
|
|
17
|
+
while (current) {
|
|
18
|
+
const variable = current.set?.get(name);
|
|
19
|
+
if (variable) return variable;
|
|
20
|
+
current = current.upper;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function resolvesToImportedGetter(name, scope) {
|
|
25
|
+
if (!getterNames.has(name)) return false;
|
|
26
|
+
const calleeVar = resolveVariable(scope, name);
|
|
27
|
+
return calleeVar?.defs?.some((d) => d.type === "ImportBinding") ?? false;
|
|
28
|
+
}
|
|
29
|
+
function isLoggerExpression(node, scope, depth = 0) {
|
|
30
|
+
if (depth > 16 || !node) return false;
|
|
31
|
+
if (node.type === "CallExpression" && node.callee?.type === "Identifier") return resolvesToImportedGetter(node.callee.name, scope);
|
|
32
|
+
if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && node.callee.property?.type === "Identifier" && (node.callee.property.name === "with" || node.callee.property.name === "getChild")) return isLoggerExpression(node.callee.object, scope, depth + 1);
|
|
33
|
+
if (node.type === "Identifier") {
|
|
34
|
+
const variable = resolveVariable(scope, node.name);
|
|
35
|
+
if (!variable) return false;
|
|
36
|
+
return variable.defs?.some((def) => {
|
|
37
|
+
if (def.type !== "Variable") return false;
|
|
38
|
+
return isLoggerExpression(def.node?.init, variable.scope ?? scope, depth + 1);
|
|
39
|
+
}) ?? false;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function isLogtapeCall(callee, callNode) {
|
|
44
|
+
if (!callee || callee.type !== "MemberExpression") return false;
|
|
45
|
+
const scope = context.sourceCode?.getScope?.(callNode) ?? context.getScope?.();
|
|
46
|
+
return isLoggerExpression(callee.object, scope);
|
|
47
|
+
}
|
|
48
|
+
function effectiveLazyNames(callNode) {
|
|
49
|
+
if (lazyNames.size === 0) return lazyNames;
|
|
50
|
+
const scope = context.sourceCode?.getScope?.(callNode) ?? context.getScope?.();
|
|
51
|
+
const result = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const name of lazyNames) {
|
|
53
|
+
const variable = resolveVariable(scope, name);
|
|
54
|
+
const isImport = variable?.defs?.some((d) => d.type === "ImportBinding") ?? false;
|
|
55
|
+
if (isImport) result.add(name);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
ImportDeclaration(node) {
|
|
61
|
+
if (!isLogtapeImportSource(node.source?.value)) return;
|
|
62
|
+
for (const spec of node.specifiers ?? []) {
|
|
63
|
+
if (spec.type !== "ImportSpecifier") continue;
|
|
64
|
+
if (spec.imported?.name === "getLogger") getterNames.add(spec.local?.name);
|
|
65
|
+
else if (spec.imported?.name === "lazy") lazyNames.add(spec.local?.name);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
isLogtapeCall,
|
|
69
|
+
lazyNames,
|
|
70
|
+
effectiveLazyNames
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the SourceCode object from a rule context (v8/v9 compatible).
|
|
75
|
+
*/
|
|
76
|
+
function getSourceCode(context) {
|
|
77
|
+
return context.sourceCode ?? context.getSourceCode?.();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
export { createLogtapeScope, getSourceCode };
|
|
82
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","names":["context: Rule.RuleContext","scope: AnyNode","name: string","d: AnyNode","node: AnyNode","def: AnyNode","callee: AnyNode","callNode: AnyNode"],"sources":["../src/utils.ts"],"sourcesContent":["import type { Rule } from \"eslint\";\nimport { isLogtapeImportSource } from \"./core/ast.ts\";\n\n// deno-lint-ignore no-explicit-any\ntype AnyNode = any;\n\n/**\n * Returns visitor hooks and a scope-aware checker for detecting LogTape log\n * method calls. Uses ESLint's scope manager to resolve variable bindings,\n * so parameter- and local-variable shadowing are handled correctly.\n *\n * Wire the returned `ImportDeclaration` hook into your rule's visitor object,\n * then call `isLogtapeCall(node.callee, node)` inside `CallExpression`.\n */\nexport function createLogtapeScope(context: Rule.RuleContext): {\n ImportDeclaration(node: AnyNode): void;\n isLogtapeCall(callee: AnyNode, callNode: AnyNode): boolean;\n lazyNames: Set<string>;\n effectiveLazyNames(callNode: AnyNode): Set<string>;\n} {\n const getterNames = new Set<string>();\n // Local names of `lazy` imported from @logtape/logtape. A lazy() value is\n // already deferred, so the eager-call rules must not treat it as eager.\n const lazyNames = new Set<string>();\n\n function resolveVariable(scope: AnyNode, name: string): AnyNode {\n let current = scope;\n while (current) {\n const variable = current.set?.get(name);\n if (variable) return variable;\n current = current.upper;\n }\n return null;\n }\n\n // Does `name`, resolved from `scope`, refer to the actual imported\n // getLogger (an ImportBinding) rather than a shadow such as a parameter?\n function resolvesToImportedGetter(name: string, scope: AnyNode): boolean {\n if (!getterNames.has(name)) return false;\n const calleeVar = resolveVariable(scope, name);\n return calleeVar?.defs?.some(\n (d: AnyNode) => d.type === \"ImportBinding\",\n ) ?? false;\n }\n\n // Is `node` an expression that evaluates to a LogTape logger? Handles\n // direct getLogger(...) calls, identifiers bound to a logger via a variable\n // declaration, and contextual/child loggers produced by chaining\n // Logger.with(...) or Logger.getChild(...).\n function isLoggerExpression(\n node: AnyNode,\n scope: AnyNode,\n depth = 0,\n ): boolean {\n if (depth > 16 || !node) return false;\n\n // getLogger(...) — direct call to the imported getter.\n if (node.type === \"CallExpression\" && node.callee?.type === \"Identifier\") {\n return resolvesToImportedGetter(node.callee.name, scope);\n }\n\n // logger.with(...) / logger.getChild(...) — contextual or child loggers,\n // which return a Logger and so are themselves logger expressions.\n if (\n node.type === \"CallExpression\" &&\n node.callee?.type === \"MemberExpression\" &&\n !node.callee.computed &&\n node.callee.property?.type === \"Identifier\" &&\n (node.callee.property.name === \"with\" ||\n node.callee.property.name === \"getChild\")\n ) {\n return isLoggerExpression(node.callee.object, scope, depth + 1);\n }\n\n // An identifier bound to a logger by a variable declaration.\n if (node.type === \"Identifier\") {\n const variable = resolveVariable(scope, node.name);\n if (!variable) return false;\n // Parameters have def.type === \"Parameter\"; only \"Variable\" defs from\n // VariableDeclarator nodes carry an initializer to inspect. Resolve the\n // initializer in the variable's declaration scope (variable.scope) so a\n // logger declared in an outer scope is still recognised when a closer\n // scope shadows getLogger.\n return variable.defs?.some((def: AnyNode) => {\n if (def.type !== \"Variable\") return false;\n return isLoggerExpression(\n def.node?.init,\n variable.scope ?? scope,\n depth + 1,\n );\n }) ?? false;\n }\n\n return false;\n }\n\n function isLogtapeCall(callee: AnyNode, callNode: AnyNode): boolean {\n if (!callee || callee.type !== \"MemberExpression\") return false;\n\n // Use ESLint's scope manager to resolve variable bindings at the call site.\n const scope =\n // deno-lint-ignore no-explicit-any\n (context as any).sourceCode?.getScope?.(callNode) ??\n // deno-lint-ignore no-explicit-any\n (context as any).getScope?.();\n\n return isLoggerExpression(callee.object, scope);\n }\n\n // The subset of lazyNames that, at `callNode`, still resolve to the imported\n // `lazy` rather than a shadowing local binding (e.g. a parameter named\n // `lazy`). The eager-call rules use this so a shadowed `lazy(...)` is\n // treated as an ordinary eager call, not LogTape's deferred wrapper.\n function effectiveLazyNames(callNode: AnyNode): Set<string> {\n if (lazyNames.size === 0) return lazyNames;\n const scope =\n // deno-lint-ignore no-explicit-any\n (context as any).sourceCode?.getScope?.(callNode) ??\n // deno-lint-ignore no-explicit-any\n (context as any).getScope?.();\n const result = new Set<string>();\n for (const name of lazyNames) {\n const variable = resolveVariable(scope, name);\n const isImport = variable?.defs?.some(\n (d: AnyNode) => d.type === \"ImportBinding\",\n ) ?? false;\n if (isImport) result.add(name);\n }\n return result;\n }\n\n return {\n ImportDeclaration(node: AnyNode): void {\n if (!isLogtapeImportSource(node.source?.value)) return;\n for (const spec of node.specifiers ?? []) {\n if (spec.type !== \"ImportSpecifier\") continue;\n if (spec.imported?.name === \"getLogger\") {\n getterNames.add(spec.local?.name);\n } else if (spec.imported?.name === \"lazy\") {\n lazyNames.add(spec.local?.name);\n }\n }\n },\n isLogtapeCall,\n lazyNames,\n effectiveLazyNames,\n };\n}\n\n/**\n * Get the SourceCode object from a rule context (v8/v9 compatible).\n */\nexport function getSourceCode(\n context: Rule.RuleContext,\n): Rule.RuleContext[\"sourceCode\"] {\n // deno-lint-ignore no-explicit-any\n return (context as any).sourceCode ?? context.getSourceCode?.();\n}\n"],"mappings":";;;;;;;;;;;AAcA,SAAgB,mBAAmBA,SAKjC;CACA,MAAM,8BAAc,IAAI;CAGxB,MAAM,4BAAY,IAAI;CAEtB,SAAS,gBAAgBC,OAAgBC,MAAuB;EAC9D,IAAI,UAAU;AACd,SAAO,SAAS;GACd,MAAM,WAAW,QAAQ,KAAK,IAAI,KAAK;AACvC,OAAI,SAAU,QAAO;AACrB,aAAU,QAAQ;EACnB;AACD,SAAO;CACR;CAID,SAAS,yBAAyBA,MAAcD,OAAyB;AACvE,OAAK,YAAY,IAAI,KAAK,CAAE,QAAO;EACnC,MAAM,YAAY,gBAAgB,OAAO,KAAK;AAC9C,SAAO,WAAW,MAAM,KACtB,CAACE,MAAe,EAAE,SAAS,gBAC5B,IAAI;CACN;CAMD,SAAS,mBACPC,MACAH,OACA,QAAQ,GACC;AACT,MAAI,QAAQ,OAAO,KAAM,QAAO;AAGhC,MAAI,KAAK,SAAS,oBAAoB,KAAK,QAAQ,SAAS,aAC1D,QAAO,yBAAyB,KAAK,OAAO,MAAM,MAAM;AAK1D,MACE,KAAK,SAAS,oBACd,KAAK,QAAQ,SAAS,uBACrB,KAAK,OAAO,YACb,KAAK,OAAO,UAAU,SAAS,iBAC9B,KAAK,OAAO,SAAS,SAAS,UAC7B,KAAK,OAAO,SAAS,SAAS,YAEhC,QAAO,mBAAmB,KAAK,OAAO,QAAQ,OAAO,QAAQ,EAAE;AAIjE,MAAI,KAAK,SAAS,cAAc;GAC9B,MAAM,WAAW,gBAAgB,OAAO,KAAK,KAAK;AAClD,QAAK,SAAU,QAAO;AAMtB,UAAO,SAAS,MAAM,KAAK,CAACI,QAAiB;AAC3C,QAAI,IAAI,SAAS,WAAY,QAAO;AACpC,WAAO,mBACL,IAAI,MAAM,MACV,SAAS,SAAS,OAClB,QAAQ,EACT;GACF,EAAC,IAAI;EACP;AAED,SAAO;CACR;CAED,SAAS,cAAcC,QAAiBC,UAA4B;AAClE,OAAK,UAAU,OAAO,SAAS,mBAAoB,QAAO;EAG1D,MAAM,QAEJ,AAAC,QAAgB,YAAY,WAAW,SAAS,IAE/C,AAAC,QAAgB,YAAY;AAEjC,SAAO,mBAAmB,OAAO,QAAQ,MAAM;CAChD;CAMD,SAAS,mBAAmBA,UAAgC;AAC1D,MAAI,UAAU,SAAS,EAAG,QAAO;EACjC,MAAM,QAEJ,AAAC,QAAgB,YAAY,WAAW,SAAS,IAE/C,AAAC,QAAgB,YAAY;EACjC,MAAM,yBAAS,IAAI;AACnB,OAAK,MAAM,QAAQ,WAAW;GAC5B,MAAM,WAAW,gBAAgB,OAAO,KAAK;GAC7C,MAAM,WAAW,UAAU,MAAM,KAC/B,CAACJ,MAAe,EAAE,SAAS,gBAC5B,IAAI;AACL,OAAI,SAAU,QAAO,IAAI,KAAK;EAC/B;AACD,SAAO;CACR;AAED,QAAO;EACL,kBAAkBC,MAAqB;AACrC,QAAK,sBAAsB,KAAK,QAAQ,MAAM,CAAE;AAChD,QAAK,MAAM,QAAQ,KAAK,cAAc,CAAE,GAAE;AACxC,QAAI,KAAK,SAAS,kBAAmB;AACrC,QAAI,KAAK,UAAU,SAAS,YAC1B,aAAY,IAAI,KAAK,OAAO,KAAK;aACxB,KAAK,UAAU,SAAS,OACjC,WAAU,IAAI,KAAK,OAAO,KAAK;GAElC;EACF;EACD;EACA;EACA;CACD;AACF;;;;AAKD,SAAgB,cACdJ,SACgC;AAEhC,QAAQ,QAAgB,cAAc,QAAQ,iBAAiB;AAChE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@logtape/lint",
|
|
3
|
+
"version": "2.2.0-dev.0",
|
|
4
|
+
"description": "Lint rules for LogTape usage patterns (ESLint, Oxlint, Deno Lint)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"logging",
|
|
7
|
+
"log",
|
|
8
|
+
"logger",
|
|
9
|
+
"logtape",
|
|
10
|
+
"lint",
|
|
11
|
+
"eslint",
|
|
12
|
+
"eslint-plugin",
|
|
13
|
+
"oxlint",
|
|
14
|
+
"deno"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Hong Minhee",
|
|
19
|
+
"email": "hong@minhee.org",
|
|
20
|
+
"url": "https://hongminhee.org/"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://logtape.org/",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/dahlia/logtape.git",
|
|
26
|
+
"directory": "packages/lint/"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/dahlia/logtape/issues"
|
|
30
|
+
},
|
|
31
|
+
"funding": [
|
|
32
|
+
"https://github.com/sponsors/dahlia"
|
|
33
|
+
],
|
|
34
|
+
"type": "module",
|
|
35
|
+
"module": "./dist/mod.js",
|
|
36
|
+
"main": "./dist/mod.cjs",
|
|
37
|
+
"types": "./dist/mod.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": {
|
|
41
|
+
"import": "./dist/mod.d.ts",
|
|
42
|
+
"require": "./dist/mod.d.cts"
|
|
43
|
+
},
|
|
44
|
+
"import": "./dist/mod.js",
|
|
45
|
+
"require": "./dist/mod.cjs"
|
|
46
|
+
},
|
|
47
|
+
"./eslint": {
|
|
48
|
+
"types": {
|
|
49
|
+
"import": "./dist/eslint/plugin.d.ts",
|
|
50
|
+
"require": "./dist/eslint/plugin.d.cts"
|
|
51
|
+
},
|
|
52
|
+
"import": "./dist/eslint/plugin.js",
|
|
53
|
+
"require": "./dist/eslint/plugin.cjs"
|
|
54
|
+
},
|
|
55
|
+
"./deno": {
|
|
56
|
+
"types": {
|
|
57
|
+
"import": "./dist/deno/plugin.d.ts",
|
|
58
|
+
"require": "./dist/deno/plugin.d.cts"
|
|
59
|
+
},
|
|
60
|
+
"import": "./dist/deno/plugin.js",
|
|
61
|
+
"require": "./dist/deno/plugin.cjs"
|
|
62
|
+
},
|
|
63
|
+
"./package.json": "./package.json"
|
|
64
|
+
},
|
|
65
|
+
"files": [
|
|
66
|
+
"dist/"
|
|
67
|
+
],
|
|
68
|
+
"sideEffects": false,
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"eslint": ">=8.0.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"eslint": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"eslint": "^9.0.0",
|
|
79
|
+
"tsdown": "^0.12.7",
|
|
80
|
+
"typescript": "^5.8.3"
|
|
81
|
+
},
|
|
82
|
+
"scripts": {
|
|
83
|
+
"build": "tsdown",
|
|
84
|
+
"prepublish": "tsdown",
|
|
85
|
+
"test": "tsdown && node --experimental-transform-types --test",
|
|
86
|
+
"test:bun": "tsdown && bun test",
|
|
87
|
+
"test:deno": "deno test --allow-run --allow-write --allow-read --allow-env",
|
|
88
|
+
"test-all": "tsdown && node --experimental-transform-types --test && bun test && deno test --allow-run --allow-write --allow-read --allow-env"
|
|
89
|
+
}
|
|
90
|
+
}
|