react-native-nitro-logger 0.1.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/NitroLogger.podspec +29 -0
- package/README.md +214 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/AndroidPlatformIo.kt +100 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridFileSink.kt +292 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridNativeConsoleSink.kt +23 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogFileWriter.kt +1328 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogRotationPolicy.kt +186 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogSecureFile.kt +80 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogWriterRegistry.kt +420 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NativeConsoleWriter.kt +237 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NitroLoggerPackage.kt +22 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/PlatformIo.kt +165 -0
- package/docs/PARITY.md +330 -0
- package/docs/PRIVACY.md +341 -0
- package/eslint-plugin/README.md +238 -0
- package/eslint-plugin/index.js +62 -0
- package/eslint-plugin/rules/literal-subsystem.js +102 -0
- package/eslint-plugin/rules/no-computed-metadata-key.js +174 -0
- package/eslint-plugin/rules/no-derived-correlation.js +171 -0
- package/eslint-plugin/rules/no-dynamic-message.js +60 -0
- package/eslint-plugin/shared.js +1948 -0
- package/ios/Gzip.swift +203 -0
- package/ios/HybridFileSink.swift +291 -0
- package/ios/HybridNativeConsoleSink.swift +20 -0
- package/ios/LogFileWriter.swift +1280 -0
- package/ios/LogSecureFile.swift +296 -0
- package/ios/LogWriterRegistry.swift +444 -0
- package/ios/NativeConsoleWriter.swift +180 -0
- package/lib/module/Logger.js +381 -0
- package/lib/module/Logger.js.map +1 -0
- package/lib/module/ScopedLogger.js +70 -0
- package/lib/module/ScopedLogger.js.map +1 -0
- package/lib/module/config.js +23 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/destinations/Batcher.js +647 -0
- package/lib/module/destinations/Batcher.js.map +1 -0
- package/lib/module/destinations/ConsoleDestination.js +51 -0
- package/lib/module/destinations/ConsoleDestination.js.map +1 -0
- package/lib/module/destinations/FileDestination.js +360 -0
- package/lib/module/destinations/FileDestination.js.map +1 -0
- package/lib/module/destinations/NativeConsoleDestination.js +170 -0
- package/lib/module/destinations/NativeConsoleDestination.js.map +1 -0
- package/lib/module/destinations/types.js +4 -0
- package/lib/module/destinations/types.js.map +1 -0
- package/lib/module/formatters/DefaultFormatter.js +136 -0
- package/lib/module/formatters/DefaultFormatter.js.map +1 -0
- package/lib/module/formatters/JsonLinesFormatter.js +162 -0
- package/lib/module/formatters/JsonLinesFormatter.js.map +1 -0
- package/lib/module/formatters/timestamp.js +15 -0
- package/lib/module/formatters/timestamp.js.map +1 -0
- package/lib/module/formatters/types.js +4 -0
- package/lib/module/formatters/types.js.map +1 -0
- package/lib/module/index.js +31 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/integrations/appState.js +81 -0
- package/lib/module/integrations/appState.js.map +1 -0
- package/lib/module/integrations/errorHandler.js +166 -0
- package/lib/module/integrations/errorHandler.js.map +1 -0
- package/lib/module/integrations/sanitizeError.js +238 -0
- package/lib/module/integrations/sanitizeError.js.map +1 -0
- package/lib/module/levels.js +35 -0
- package/lib/module/levels.js.map +1 -0
- package/lib/module/metadata.js +52 -0
- package/lib/module/metadata.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/privacy.js +349 -0
- package/lib/module/privacy.js.map +1 -0
- package/lib/module/specs/FileSink.nitro.js +4 -0
- package/lib/module/specs/FileSink.nitro.js.map +1 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js +4 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utf8.js +42 -0
- package/lib/module/utf8.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/Logger.d.ts +122 -0
- package/lib/typescript/src/Logger.d.ts.map +1 -0
- package/lib/typescript/src/ScopedLogger.d.ts +36 -0
- package/lib/typescript/src/ScopedLogger.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +12 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/destinations/Batcher.d.ts +244 -0
- package/lib/typescript/src/destinations/Batcher.d.ts.map +1 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts +27 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts +200 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts +86 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/types.d.ts +19 -0
- package/lib/typescript/src/destinations/types.d.ts.map +1 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts +22 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts +73 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/timestamp.d.ts +6 -0
- package/lib/typescript/src/formatters/timestamp.d.ts.map +1 -0
- package/lib/typescript/src/formatters/types.d.ts +27 -0
- package/lib/typescript/src/formatters/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +34 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/integrations/appState.d.ts +33 -0
- package/lib/typescript/src/integrations/appState.d.ts.map +1 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts +55 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts.map +1 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts +89 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts.map +1 -0
- package/lib/typescript/src/levels.d.ts +9 -0
- package/lib/typescript/src/levels.d.ts.map +1 -0
- package/lib/typescript/src/metadata.d.ts +23 -0
- package/lib/typescript/src/metadata.d.ts.map +1 -0
- package/lib/typescript/src/privacy.d.ts +103 -0
- package/lib/typescript/src/privacy.d.ts.map +1 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts +131 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts +21 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +37 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utf8.d.ts +15 -0
- package/lib/typescript/src/utf8.d.ts.map +1 -0
- package/nitro.json +33 -0
- package/nitrogen/generated/android/c++/JAppendResult.hpp +79 -0
- package/nitrogen/generated/android/c++/JClearOutcome.hpp +88 -0
- package/nitrogen/generated/android/c++/JFlushOutcome.hpp +81 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.cpp +120 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.cpp +71 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JRejectReason.hpp +64 -0
- package/nitrogen/generated/android/c++/JRotationConfig.hpp +77 -0
- package/nitrogen/generated/android/c++/JSinkStatus.hpp +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/AppendResult.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/ClearOutcome.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/FlushOutcome.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridFileSinkSpec.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridNativeConsoleSinkSpec.kt +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RejectReason.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RotationConfig.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/SinkStatus.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/nitrologgerOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitrologger+autolinking.cmake +83 -0
- package/nitrogen/generated/android/nitrologger+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.cpp +70 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroLogger+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.cpp +50 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.hpp +216 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Umbrella.hpp +69 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.mm +41 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.hpp +150 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/swift/AppendResult.swift +60 -0
- package/nitrogen/generated/ios/swift/ClearOutcome.swift +50 -0
- package/nitrogen/generated/ios/swift/FlushOutcome.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec.swift +61 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec_cxx.swift +227 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec_cxx.swift +148 -0
- package/nitrogen/generated/ios/swift/RejectReason.swift +48 -0
- package/nitrogen/generated/ios/swift/RotationConfig.swift +93 -0
- package/nitrogen/generated/ios/swift/SinkStatus.swift +44 -0
- package/nitrogen/generated/shared/c++/AppendResult.hpp +105 -0
- package/nitrogen/generated/shared/c++/ClearOutcome.hpp +96 -0
- package/nitrogen/generated/shared/c++/FlushOutcome.hpp +107 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.cpp +28 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/RejectReason.hpp +84 -0
- package/nitrogen/generated/shared/c++/RotationConfig.hpp +103 -0
- package/nitrogen/generated/shared/c++/SinkStatus.hpp +95 -0
- package/package.json +151 -0
- package/src/Logger.ts +417 -0
- package/src/ScopedLogger.ts +94 -0
- package/src/config.ts +25 -0
- package/src/destinations/Batcher.ts +805 -0
- package/src/destinations/ConsoleDestination.ts +60 -0
- package/src/destinations/FileDestination.ts +473 -0
- package/src/destinations/NativeConsoleDestination.ts +220 -0
- package/src/destinations/types.ts +19 -0
- package/src/formatters/DefaultFormatter.ts +150 -0
- package/src/formatters/JsonLinesFormatter.ts +195 -0
- package/src/formatters/timestamp.ts +12 -0
- package/src/formatters/types.ts +29 -0
- package/src/index.tsx +106 -0
- package/src/integrations/appState.ts +111 -0
- package/src/integrations/errorHandler.ts +223 -0
- package/src/integrations/sanitizeError.ts +287 -0
- package/src/levels.ts +35 -0
- package/src/metadata.ts +52 -0
- package/src/privacy.ts +389 -0
- package/src/specs/FileSink.nitro.ts +144 -0
- package/src/specs/NativeConsoleSink.nitro.ts +21 -0
- package/src/types.ts +45 -0
- package/src/utf8.ts +39 -0
|
@@ -0,0 +1,1948 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared call-site analysis for the privacy lint rules.
|
|
5
|
+
*
|
|
6
|
+
* These rules enforce the "public by contract" half of the privacy design:
|
|
7
|
+
* fields the runtime cannot redact — the message, the subsystem, the
|
|
8
|
+
* correlation ID, metadata keys — have to be provably static at build time.
|
|
9
|
+
* The runtime redacts values; lint keeps everything else from becoming a
|
|
10
|
+
* value in the first place.
|
|
11
|
+
*
|
|
12
|
+
* Three rules govern everything below, and they are what make the analysis a
|
|
13
|
+
* control rather than a speed bump:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Receivers resolve permissively.** Failing to recognize a logger
|
|
16
|
+
* silences every rule at that call site, so aliases, import renames,
|
|
17
|
+
* computed access, `this` members, method references and post-declaration
|
|
18
|
+
* assignment are all followed. A receiver that merely *might* be a logger
|
|
19
|
+
* is `'ambiguous'`, not discarded.
|
|
20
|
+
* 2. **Values resolve strictly.** `const` protects the binding, not the
|
|
21
|
+
* object. A binding is a constant only if nothing writes through it AND it
|
|
22
|
+
* never escapes into code we cannot see.
|
|
23
|
+
* 3. **Unprovable is reported, never skipped.** Every position that could
|
|
24
|
+
* carry a public-by-contract field and cannot be resolved yields an
|
|
25
|
+
* `unanalyzable` result. A spread argument, an opaque options object and a
|
|
26
|
+
* computed property that could shadow a checked key are all findings, not
|
|
27
|
+
* reasons to fall silent.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const LEVEL_METHODS = new Set([
|
|
31
|
+
'verbose',
|
|
32
|
+
'debug',
|
|
33
|
+
'info',
|
|
34
|
+
'warning',
|
|
35
|
+
'error',
|
|
36
|
+
'todo',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Level helpers plus the generic entry points.
|
|
41
|
+
*
|
|
42
|
+
* `logMessage` is here because it is public, exported, and the thing `log`
|
|
43
|
+
* delegates to — `log(message, options)` is a one-line passthrough. Leaving it
|
|
44
|
+
* out made `Log.logMessage(`MRN ${x}`, ...)` lint clean while the identical
|
|
45
|
+
* `Log.log(...)` errored, which is not a defence-in-depth gap: message,
|
|
46
|
+
* correlation and subsystem have no runtime redaction anywhere, so these rules
|
|
47
|
+
* are the only thing standing between them and a log file.
|
|
48
|
+
*
|
|
49
|
+
* Anything emitting added to `Logger` must land in this set. That is not left
|
|
50
|
+
* to memory — see the exhaustiveness test in `__tests__/eslintPlugin.test.js`,
|
|
51
|
+
* which fails on a prototype method it cannot classify.
|
|
52
|
+
*/
|
|
53
|
+
const LOG_METHODS = new Set([...LEVEL_METHODS, 'log', 'logMessage']);
|
|
54
|
+
|
|
55
|
+
/** Level configuration, which only the Logger singleton exposes. */
|
|
56
|
+
const CONFIG_METHODS = new Set(['subsystem', 'resetSubsystem']);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Methods whose second argument is a `LogOptions` object rather than metadata.
|
|
60
|
+
*
|
|
61
|
+
* `Log.log` is only this shape when its receiver resolves to the Logger — a
|
|
62
|
+
* ScopedLogger's `log` takes `(message, level, metadata)`. `logMessage` has no
|
|
63
|
+
* such overload, so it is always the options shape.
|
|
64
|
+
*/
|
|
65
|
+
const OPTIONS_METHODS = new Set(['log', 'logMessage']);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fields on a `LogOptions` object that carry caller metadata.
|
|
69
|
+
*
|
|
70
|
+
* Both reach `redactMetadata` and both are equally public — `scopeMetadata` is
|
|
71
|
+
* what `ScopedLogger` threads through, and nothing stops an application from
|
|
72
|
+
* passing it directly. A rule that read only `metadata` left the other half of
|
|
73
|
+
* the same pipeline unchecked.
|
|
74
|
+
*/
|
|
75
|
+
const METADATA_OPTION_FIELDS = ['metadata', 'scopeMetadata'];
|
|
76
|
+
|
|
77
|
+
/** Every method whose arguments are public by contract. */
|
|
78
|
+
const API_METHODS = new Set([...LOG_METHODS, ...CONFIG_METHODS, 'scoped']);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Every method the package's own Logger and ScopedLogger define.
|
|
82
|
+
*
|
|
83
|
+
* Kept as a closed list, not "any method on a trusted receiver", because
|
|
84
|
+
* `Object.prototype` lends every object methods the package never wrote:
|
|
85
|
+
* `Log.__defineGetter__('newCorrelationId', () => () => patient.mrn)`
|
|
86
|
+
* replaces the generator through a call that mutates nothing the package
|
|
87
|
+
* knows about. A call to anything absent from this list has to count as
|
|
88
|
+
* tampering. Mirrors src/Logger.ts and src/ScopedLogger.ts.
|
|
89
|
+
*/
|
|
90
|
+
const LOGGER_OWN_METHODS = new Set([
|
|
91
|
+
...API_METHODS,
|
|
92
|
+
'addDestination',
|
|
93
|
+
'consoleLogging',
|
|
94
|
+
'flush',
|
|
95
|
+
'logMessage',
|
|
96
|
+
'metadataKeyCatalog',
|
|
97
|
+
'minimumLevel',
|
|
98
|
+
'newCorrelationId',
|
|
99
|
+
'privacyDefault',
|
|
100
|
+
'redactAllMetadata',
|
|
101
|
+
'removeDestination',
|
|
102
|
+
// TypeScript `private` is erased, so these are ordinary prototype methods at
|
|
103
|
+
// runtime. They belong here for the same reason as the rest: they are the
|
|
104
|
+
// package's own code, so a call to one is not tampering.
|
|
105
|
+
'noteFailure',
|
|
106
|
+
'privacySettings',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const DEFAULT_LOGGER_NAMES = ['Log', 'log', 'logger'];
|
|
110
|
+
|
|
111
|
+
function configuredLoggerNames(context) {
|
|
112
|
+
const configured = context.options?.[0]?.loggerNames;
|
|
113
|
+
return Array.isArray(configured) && configured.length > 0
|
|
114
|
+
? configured
|
|
115
|
+
: DEFAULT_LOGGER_NAMES;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function loggerNames(context) {
|
|
119
|
+
return new Set(configuredLoggerNames(context));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The one name that means the Logger singleton itself, rather than "some
|
|
124
|
+
* logger-shaped thing".
|
|
125
|
+
*
|
|
126
|
+
* `Log` is the documented export; `log` and `logger` are what people call
|
|
127
|
+
* whatever they have to hand, very often a ScopedLogger. An unresolved
|
|
128
|
+
* `Log.log(…)` can be assumed to have the Logger's argument order; an
|
|
129
|
+
* unresolved `logger.log(…)` cannot.
|
|
130
|
+
*
|
|
131
|
+
* Configurable on its own rather than by `loggerNames` array order, so that
|
|
132
|
+
* reordering that list cannot quietly change which name carries the meaning.
|
|
133
|
+
* This decides argument SHAPE only — trusting a value still needs
|
|
134
|
+
* {@link isTrustedLogger}, which requires a verified import.
|
|
135
|
+
*/
|
|
136
|
+
function canonicalLoggerName(context) {
|
|
137
|
+
return (
|
|
138
|
+
context.options?.[0]?.singletonName ?? configuredLoggerNames(context)[0]
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Modules an import of the Logger may legitimately come from. */
|
|
143
|
+
const DEFAULT_LOGGER_MODULES = ['react-native-nitro-logger'];
|
|
144
|
+
|
|
145
|
+
function loggerModules(context) {
|
|
146
|
+
const configured = context.options?.[0]?.loggerModules;
|
|
147
|
+
return new Set(
|
|
148
|
+
Array.isArray(configured) && configured.length > 0
|
|
149
|
+
? configured
|
|
150
|
+
: DEFAULT_LOGGER_MODULES
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Was this binding imported from a module that actually ships the Logger?
|
|
156
|
+
*
|
|
157
|
+
* A name is not provenance. `import { Log } from './phi-helpers'` binds the
|
|
158
|
+
* documented name to something we know nothing about, and treating it as the
|
|
159
|
+
* singleton would let its `newCorrelationId()` mint approved IDs out of MRNs.
|
|
160
|
+
*/
|
|
161
|
+
function isLoggerImport(context, def) {
|
|
162
|
+
if (!def || def.type !== 'ImportBinding') return false;
|
|
163
|
+
const imported = def.node.imported?.name ?? def.node.local?.name;
|
|
164
|
+
if (!loggerNames(context).has(imported)) return false;
|
|
165
|
+
const source = def.parent?.source?.value;
|
|
166
|
+
return typeof source === 'string' && loggerModules(context).has(source);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* -------------------------------------------------------------------------
|
|
170
|
+
* Syntax normalization
|
|
171
|
+
* ---------------------------------------------------------------------- */
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Strip the wrappers that change an expression's spelling but not its value:
|
|
175
|
+
* TypeScript assertions, non-null assertions, optional-chain envelopes and
|
|
176
|
+
* the comma operator. Each is a one-token bypass of a naive AST match —
|
|
177
|
+
* `(0, Log.info)(msg)` and `(Log as any)!.info(msg)` must not read
|
|
178
|
+
* differently from `Log.info(msg)`.
|
|
179
|
+
*/
|
|
180
|
+
function unwrap(node) {
|
|
181
|
+
let current = node ?? null;
|
|
182
|
+
for (;;) {
|
|
183
|
+
if (!current) return null;
|
|
184
|
+
switch (current.type) {
|
|
185
|
+
case 'TSAsExpression':
|
|
186
|
+
case 'TSSatisfiesExpression':
|
|
187
|
+
case 'TSNonNullExpression':
|
|
188
|
+
case 'TSTypeAssertion':
|
|
189
|
+
case 'TSInstantiationExpression':
|
|
190
|
+
case 'ChainExpression':
|
|
191
|
+
current = current.expression;
|
|
192
|
+
break;
|
|
193
|
+
case 'SequenceExpression':
|
|
194
|
+
current = current.expressions[current.expressions.length - 1];
|
|
195
|
+
break;
|
|
196
|
+
default:
|
|
197
|
+
return current;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The key of an object-literal property, or null when computed. */
|
|
203
|
+
function propertyKeyName(property) {
|
|
204
|
+
if (property.computed) return null;
|
|
205
|
+
const key = property.key;
|
|
206
|
+
if (key.type === 'Identifier') return key.name;
|
|
207
|
+
if (key.type === 'Literal') return String(key.value);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* -------------------------------------------------------------------------
|
|
212
|
+
* Binding resolution
|
|
213
|
+
* ---------------------------------------------------------------------- */
|
|
214
|
+
|
|
215
|
+
function resolveVariable(context, identifier) {
|
|
216
|
+
let scope = context.sourceCode.getScope(identifier);
|
|
217
|
+
while (scope) {
|
|
218
|
+
const found = scope.variables.find((v) => v.name === identifier.name);
|
|
219
|
+
if (found) return found;
|
|
220
|
+
scope = scope.upper;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function singleDef(variable) {
|
|
226
|
+
return variable && variable.defs.length === 1 ? variable.defs[0] : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The one value a binding is ever given, whether at its declaration or by a
|
|
231
|
+
* single later assignment.
|
|
232
|
+
*
|
|
233
|
+
* Receivers only. `let sink; sink = Log; sink.info(msg)` is a logger, and
|
|
234
|
+
* `let s = Log.scoped(id)` is a scope — refusing to follow either would mean
|
|
235
|
+
* one keyword disables every rule.
|
|
236
|
+
*/
|
|
237
|
+
function bindingInit(variable) {
|
|
238
|
+
const def = singleDef(variable);
|
|
239
|
+
if (!def || def.type !== 'Variable') return null;
|
|
240
|
+
|
|
241
|
+
const pattern = def.node.id;
|
|
242
|
+
if (def.node.init) {
|
|
243
|
+
const init = unwrap(def.node.init);
|
|
244
|
+
if (pattern.type === 'Identifier') return init;
|
|
245
|
+
// `const [a] = [Log]` — follow the element this binding actually took.
|
|
246
|
+
if (
|
|
247
|
+
pattern.type === 'ArrayPattern' &&
|
|
248
|
+
init &&
|
|
249
|
+
init.type === 'ArrayExpression'
|
|
250
|
+
) {
|
|
251
|
+
const index = pattern.elements.indexOf(def.name);
|
|
252
|
+
const element = index < 0 ? null : init.elements[index];
|
|
253
|
+
return element ? unwrap(element) : null;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Declared empty, then assigned exactly once.
|
|
259
|
+
const writes = variable.references.filter(
|
|
260
|
+
(reference) => reference.isWrite() && reference.writeExpr
|
|
261
|
+
);
|
|
262
|
+
return writes.length === 1 ? unwrap(writes[0].writeExpr) : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The initializer of a single-definition `const`.
|
|
267
|
+
*
|
|
268
|
+
* Values only. Unlike {@link bindingInit} this refuses `let`, because an
|
|
269
|
+
* approved constant that can be reassigned is not a constant.
|
|
270
|
+
*/
|
|
271
|
+
function immutableInit(variable) {
|
|
272
|
+
const def = singleDef(variable);
|
|
273
|
+
if (!def || def.type !== 'Variable' || def.parent?.kind !== 'const') {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
return def.node.init ? unwrap(def.node.init) : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Is this node in a position that gets written to?
|
|
281
|
+
*
|
|
282
|
+
* Simple assignment is the obvious case, but a member expression can also be
|
|
283
|
+
* a destructuring target (`({ x: M.up } = leak)`, `[M.up] = [leak]`) or a
|
|
284
|
+
* loop binding (`for (M.up of xs)`), and those reach through arbitrarily
|
|
285
|
+
* nested patterns. So climb while the node stays in target position and
|
|
286
|
+
* decide at whatever construct the pattern terminates in.
|
|
287
|
+
*/
|
|
288
|
+
function isWriteTarget(node) {
|
|
289
|
+
let current = node;
|
|
290
|
+
let parent = current.parent;
|
|
291
|
+
while (parent) {
|
|
292
|
+
switch (parent.type) {
|
|
293
|
+
case 'AssignmentExpression':
|
|
294
|
+
return parent.left === current;
|
|
295
|
+
case 'ForOfStatement':
|
|
296
|
+
case 'ForInStatement':
|
|
297
|
+
return parent.left === current;
|
|
298
|
+
case 'UpdateExpression':
|
|
299
|
+
return parent.argument === current;
|
|
300
|
+
case 'UnaryExpression':
|
|
301
|
+
return parent.operator === 'delete' && parent.argument === current;
|
|
302
|
+
// Still inside a pattern — keep climbing toward whatever assigns it.
|
|
303
|
+
case 'AssignmentPattern':
|
|
304
|
+
if (parent.left !== current) return false;
|
|
305
|
+
break;
|
|
306
|
+
case 'Property':
|
|
307
|
+
if (parent.value !== current) return false;
|
|
308
|
+
break;
|
|
309
|
+
case 'ObjectPattern':
|
|
310
|
+
case 'ArrayPattern':
|
|
311
|
+
case 'RestElement':
|
|
312
|
+
break;
|
|
313
|
+
default:
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
current = parent;
|
|
317
|
+
parent = current.parent;
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* `Object.*` and `Reflect.*` statics that hand the object back unchanged
|
|
324
|
+
* *without running any of its code*.
|
|
325
|
+
*
|
|
326
|
+
* The second half of that is the load-bearing part. `Object.values`,
|
|
327
|
+
* `Object.entries`, `Reflect.get` and `JSON.stringify` all look like reads and
|
|
328
|
+
* are not: they invoke getters (and `toJSON`), so
|
|
329
|
+
* `const M = { up: 'safe', get x() { this.up = patientName } }; Object.values(M)`
|
|
330
|
+
* rewrites `M.up` before it is logged. Only operations that touch keys and
|
|
331
|
+
* descriptors rather than values are listed here.
|
|
332
|
+
*
|
|
333
|
+
* Everything else on those namespaces — `assign`, `defineProperty`, `set`,
|
|
334
|
+
* `deleteProperty`, `setPrototypeOf` — rewrites the object outright.
|
|
335
|
+
*/
|
|
336
|
+
const NON_MUTATING_STATICS = new Set([
|
|
337
|
+
'keys',
|
|
338
|
+
'freeze',
|
|
339
|
+
'isFrozen',
|
|
340
|
+
'seal',
|
|
341
|
+
'isSealed',
|
|
342
|
+
'getOwnPropertyNames',
|
|
343
|
+
'getOwnPropertyDescriptor',
|
|
344
|
+
'getOwnPropertyDescriptors',
|
|
345
|
+
'getPrototypeOf',
|
|
346
|
+
'has',
|
|
347
|
+
'ownKeys',
|
|
348
|
+
]);
|
|
349
|
+
const BUILTIN_NAMESPACES = new Set(['Object', 'Reflect', 'JSON']);
|
|
350
|
+
const GLOBAL_OBJECT_NAMES = new Set(['globalThis', 'global', 'window', 'self']);
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Builtin namespaces this file tampers with, in any of the ways that leave
|
|
354
|
+
* the binding itself looking pristine.
|
|
355
|
+
*
|
|
356
|
+
* `const JSON = {…}` is a shadow and resolves as a definition, so scope
|
|
357
|
+
* resolution catches it. These do not:
|
|
358
|
+
*
|
|
359
|
+
* - `Object.keys = (o) => { o.up = patientName; return [] }` — the namespace
|
|
360
|
+
* is the real one; a single method on it is not.
|
|
361
|
+
* - `globalThis.JSON = fake` and `globalThis['JSON'] = fake` — replacement
|
|
362
|
+
* through the global object.
|
|
363
|
+
* - `globalThis[name] = fake` — an unreadable key, so every namespace has to
|
|
364
|
+
* be assumed replaced.
|
|
365
|
+
* - `Object.defineProperty(Object, 'keys', …)`, `Object.assign(Object, …)`,
|
|
366
|
+
* `tamper(Object)` — handing the namespace to code that can rewrite it.
|
|
367
|
+
*
|
|
368
|
+
* One walk per file, cached.
|
|
369
|
+
*/
|
|
370
|
+
const poisonedGlobalsCache = new WeakMap();
|
|
371
|
+
const ALL_NAMESPACES_POISONED = Symbol('all');
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Walk `a.b.c` down to its base identifier, collecting the property path.
|
|
375
|
+
* Unreadable (computed, non-literal) steps come back as null so the caller
|
|
376
|
+
* can decide how much to assume.
|
|
377
|
+
*/
|
|
378
|
+
function memberChain(node) {
|
|
379
|
+
const path = [];
|
|
380
|
+
let current = unwrap(node);
|
|
381
|
+
while (current && current.type === 'MemberExpression') {
|
|
382
|
+
if (!current.computed && current.property.type === 'Identifier') {
|
|
383
|
+
path.unshift(current.property.name);
|
|
384
|
+
} else {
|
|
385
|
+
const literal = unwrap(current.property);
|
|
386
|
+
path.unshift(
|
|
387
|
+
literal && literal.type === 'Literal' ? String(literal.value) : null
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
current = unwrap(current.object);
|
|
391
|
+
}
|
|
392
|
+
return { root: current, path };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Does this expression name a builtin namespace, directly or through a const
|
|
397
|
+
* alias?
|
|
398
|
+
*
|
|
399
|
+
* The binding is resolved BEFORE the spelling is trusted. A local
|
|
400
|
+
* `function f() { const Object = {}; Object.keys = fake }` writes to its own
|
|
401
|
+
* shadow, and reading that as tampering with the global would disable
|
|
402
|
+
* `Object.keys` for the whole file — a false positive in exactly the code
|
|
403
|
+
* most likely to be doing something ordinary.
|
|
404
|
+
*/
|
|
405
|
+
function builtinNamespaceOf(context, node, seen = new Set()) {
|
|
406
|
+
const current = unwrap(node);
|
|
407
|
+
if (!current) return null;
|
|
408
|
+
|
|
409
|
+
// `globalThis.Object`, and `const O = globalThis.Object` behind it.
|
|
410
|
+
if (current.type === 'MemberExpression') {
|
|
411
|
+
const { root, path } = memberChain(current);
|
|
412
|
+
if (path.length !== 1 || path[0] === null) return null;
|
|
413
|
+
if (!BUILTIN_NAMESPACES.has(path[0])) return null;
|
|
414
|
+
return isGlobalObjectRef(context, root) ? path[0] : null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (current.type !== 'Identifier') return null;
|
|
418
|
+
const variable = resolveVariable(context, current);
|
|
419
|
+
if (!variable || variable.defs.length === 0) {
|
|
420
|
+
return BUILTIN_NAMESPACES.has(current.name) ? current.name : null;
|
|
421
|
+
}
|
|
422
|
+
if (seen.has(variable)) return null;
|
|
423
|
+
seen.add(variable);
|
|
424
|
+
return builtinNamespaceOf(context, immutableInit(variable), seen);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Likewise for the global object: `globalThis`, or a const alias of it. */
|
|
428
|
+
function isGlobalObjectRef(context, node, seen = new Set()) {
|
|
429
|
+
const current = unwrap(node);
|
|
430
|
+
if (!current || current.type !== 'Identifier') return false;
|
|
431
|
+
const variable = resolveVariable(context, current);
|
|
432
|
+
if (!variable || variable.defs.length === 0) {
|
|
433
|
+
return GLOBAL_OBJECT_NAMES.has(current.name);
|
|
434
|
+
}
|
|
435
|
+
if (seen.has(variable)) return false;
|
|
436
|
+
seen.add(variable);
|
|
437
|
+
return isGlobalObjectRef(context, immutableInit(variable), seen);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function poisonedGlobals(context) {
|
|
441
|
+
const cached = poisonedGlobalsCache.get(context);
|
|
442
|
+
if (cached) return cached;
|
|
443
|
+
|
|
444
|
+
const poisoned = new Set();
|
|
445
|
+
// A namespace handed to a function can be wrapped on the way:
|
|
446
|
+
// `tamper(...[Object])` and `tamper({ ns: Object })` pass the real thing.
|
|
447
|
+
const poisonNamespaceOf = (node, seen = new Set()) => {
|
|
448
|
+
const current = unwrap(node);
|
|
449
|
+
if (!current || seen.has(current)) return;
|
|
450
|
+
seen.add(current);
|
|
451
|
+
// A cycle or a pathological nesting depth must not end the search
|
|
452
|
+
// quietly: an unfinished search is exactly the case where a namespace
|
|
453
|
+
// could be in there somewhere.
|
|
454
|
+
if (seen.size > 64) {
|
|
455
|
+
poisoned.add(ALL_NAMESPACES_POISONED);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const name = builtinNamespaceOf(context, current);
|
|
460
|
+
if (name !== null) {
|
|
461
|
+
poisoned.add(name);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
switch (current.type) {
|
|
465
|
+
case 'SpreadElement':
|
|
466
|
+
poisonNamespaceOf(current.argument, seen);
|
|
467
|
+
return;
|
|
468
|
+
case 'ArrayExpression':
|
|
469
|
+
for (const element of current.elements) {
|
|
470
|
+
poisonNamespaceOf(element, seen);
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
case 'ObjectExpression':
|
|
474
|
+
for (const property of current.properties) {
|
|
475
|
+
poisonNamespaceOf(
|
|
476
|
+
property.type === 'SpreadElement'
|
|
477
|
+
? property.argument
|
|
478
|
+
: property.value,
|
|
479
|
+
seen
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
case 'ConditionalExpression':
|
|
484
|
+
poisonNamespaceOf(current.consequent, seen);
|
|
485
|
+
poisonNamespaceOf(current.alternate, seen);
|
|
486
|
+
return;
|
|
487
|
+
case 'LogicalExpression':
|
|
488
|
+
poisonNamespaceOf(current.left, seen);
|
|
489
|
+
poisonNamespaceOf(current.right, seen);
|
|
490
|
+
return;
|
|
491
|
+
case 'Identifier': {
|
|
492
|
+
// `const namespaces = { object: Object }; tamper(namespaces)` — the
|
|
493
|
+
// wrapper is a name, and the namespace is inside what it holds.
|
|
494
|
+
const variable = resolveVariable(context, current);
|
|
495
|
+
if (!variable || seen.has(variable)) return;
|
|
496
|
+
seen.add(variable);
|
|
497
|
+
poisonNamespaceOf(immutableInit(variable), seen);
|
|
498
|
+
|
|
499
|
+
// The wrapper may have been filled in after it was declared:
|
|
500
|
+
// `const bag = {}; bag.namespace = Object; tamper(bag)`. Its
|
|
501
|
+
// declaration says nothing, so the writes have to be read too —
|
|
502
|
+
// at any depth (`bag.inner.namespace = Object`) and through any
|
|
503
|
+
// alias (`const b = bag; b.namespace = Object`).
|
|
504
|
+
for (const reference of variable.references) {
|
|
505
|
+
const parent = reference.identifier.parent;
|
|
506
|
+
if (!parent) continue;
|
|
507
|
+
|
|
508
|
+
if (
|
|
509
|
+
parent.type === 'VariableDeclarator' &&
|
|
510
|
+
parent.init === reference.identifier &&
|
|
511
|
+
parent.id.type === 'Identifier'
|
|
512
|
+
) {
|
|
513
|
+
poisonNamespaceOf(parent.id, seen);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (
|
|
518
|
+
parent.type !== 'MemberExpression' ||
|
|
519
|
+
parent.object !== reference.identifier
|
|
520
|
+
) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
let member = parent;
|
|
524
|
+
for (;;) {
|
|
525
|
+
const owner = member.parent;
|
|
526
|
+
if (!owner) break;
|
|
527
|
+
if (
|
|
528
|
+
owner.type === 'AssignmentExpression' &&
|
|
529
|
+
owner.left === member
|
|
530
|
+
) {
|
|
531
|
+
poisonNamespaceOf(owner.right, seen);
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
if (owner.type === 'MemberExpression' && owner.object === member) {
|
|
535
|
+
member = owner;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
default:
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const poisonWriteTarget = (target) => {
|
|
548
|
+
const { root, path } = memberChain(target);
|
|
549
|
+
if (!root || root.type !== 'Identifier' || path.length === 0) return;
|
|
550
|
+
|
|
551
|
+
// `Object.keys = …`, and `const O = Object; O.keys = …` — a method on the
|
|
552
|
+
// real namespace, however the namespace was spelled.
|
|
553
|
+
const namespace = builtinNamespaceOf(context, root);
|
|
554
|
+
if (namespace !== null) {
|
|
555
|
+
poisoned.add(namespace);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// `globalThis.JSON = …`, `globalThis['JSON'].parse = …`,
|
|
560
|
+
// `const g = globalThis; g.JSON = …` — replacement reached through the
|
|
561
|
+
// global object, at any depth.
|
|
562
|
+
if (!isGlobalObjectRef(context, root)) return;
|
|
563
|
+
const key = path[0];
|
|
564
|
+
if (key === null) poisoned.add(ALL_NAMESPACES_POISONED);
|
|
565
|
+
else if (BUILTIN_NAMESPACES.has(key)) poisoned.add(key);
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// A write target can be a whole destructuring pattern:
|
|
569
|
+
// `({ keys: Object.keys } = { keys: fake })` replaces the builtin without
|
|
570
|
+
// any assignment whose left side is a member expression.
|
|
571
|
+
const poisonPattern = (node) => {
|
|
572
|
+
if (!node) return;
|
|
573
|
+
switch (node.type) {
|
|
574
|
+
case 'ObjectPattern':
|
|
575
|
+
for (const property of node.properties) {
|
|
576
|
+
poisonPattern(
|
|
577
|
+
property.type === 'RestElement' ? property.argument : property.value
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
return;
|
|
581
|
+
case 'ArrayPattern':
|
|
582
|
+
for (const element of node.elements) poisonPattern(element);
|
|
583
|
+
return;
|
|
584
|
+
case 'AssignmentPattern':
|
|
585
|
+
poisonPattern(node.left);
|
|
586
|
+
return;
|
|
587
|
+
case 'RestElement':
|
|
588
|
+
poisonPattern(node.argument);
|
|
589
|
+
return;
|
|
590
|
+
default:
|
|
591
|
+
poisonWriteTarget(node);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const seenNodes = new Set();
|
|
596
|
+
const visit = (node) => {
|
|
597
|
+
if (!node || typeof node.type !== 'string' || seenNodes.has(node)) return;
|
|
598
|
+
seenNodes.add(node);
|
|
599
|
+
|
|
600
|
+
switch (node.type) {
|
|
601
|
+
case 'AssignmentExpression':
|
|
602
|
+
poisonPattern(node.left);
|
|
603
|
+
break;
|
|
604
|
+
case 'ForOfStatement':
|
|
605
|
+
case 'ForInStatement':
|
|
606
|
+
// `for (Object.keys of [fake]) {}`. A `VariableDeclaration` here
|
|
607
|
+
// declares a fresh binding and falls through harmlessly.
|
|
608
|
+
poisonPattern(node.left);
|
|
609
|
+
break;
|
|
610
|
+
case 'UpdateExpression':
|
|
611
|
+
poisonWriteTarget(node.argument);
|
|
612
|
+
break;
|
|
613
|
+
case 'UnaryExpression':
|
|
614
|
+
if (node.operator === 'delete') poisonWriteTarget(node.argument);
|
|
615
|
+
break;
|
|
616
|
+
case 'CallExpression':
|
|
617
|
+
case 'NewExpression':
|
|
618
|
+
// A namespace handed to a function is a namespace that function can
|
|
619
|
+
// rewrite, whatever the function is.
|
|
620
|
+
for (const argument of node.arguments) poisonNamespaceOf(argument);
|
|
621
|
+
break;
|
|
622
|
+
default:
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
for (const key of Object.keys(node)) {
|
|
627
|
+
if (key === 'parent') continue;
|
|
628
|
+
const value = node[key];
|
|
629
|
+
if (Array.isArray(value)) value.forEach(visit);
|
|
630
|
+
else if (value && typeof value.type === 'string') visit(value);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
visit((context.sourceCode ?? context.getSourceCode()).ast);
|
|
634
|
+
|
|
635
|
+
poisonedGlobalsCache.set(context, poisoned);
|
|
636
|
+
return poisoned;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Is this identifier an untouched global with the given name? */
|
|
640
|
+
function isGlobalNamed(context, node, names) {
|
|
641
|
+
if (!node || node.type !== 'Identifier' || !names.has(node.name))
|
|
642
|
+
return false;
|
|
643
|
+
const poisoned = poisonedGlobals(context);
|
|
644
|
+
if (poisoned.has(ALL_NAMESPACES_POISONED) || poisoned.has(node.name)) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
const variable = resolveVariable(context, node);
|
|
648
|
+
// Absent, or present as a declared global with no definition of its own.
|
|
649
|
+
return !variable || variable.defs.length === 0;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* A read-only builtin static, like `Object.keys(M)`.
|
|
654
|
+
*
|
|
655
|
+
* The namespace has to be the real global: `const JSON = { stringify: (o) =>
|
|
656
|
+
* { o.up = leak; return ''; } }` would otherwise launder a mutation through
|
|
657
|
+
* the allowlist.
|
|
658
|
+
*/
|
|
659
|
+
function isNonMutatingStatic(context, node) {
|
|
660
|
+
const callee = unwrap(node.callee);
|
|
661
|
+
if (!callee || callee.type !== 'MemberExpression' || callee.computed) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
return (
|
|
665
|
+
isGlobalNamed(context, unwrap(callee.object), BUILTIN_NAMESPACES) &&
|
|
666
|
+
callee.property.type === 'Identifier' &&
|
|
667
|
+
NON_MUTATING_STATICS.has(callee.property.name)
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Can this call mutate an object handed to it?
|
|
673
|
+
*
|
|
674
|
+
* A real logger call cannot — passing a metadata object to `Log.info` is the
|
|
675
|
+
* entire point of having one — and neither can the read-only builtin statics.
|
|
676
|
+
* Anything else is a function we cannot see inside.
|
|
677
|
+
*
|
|
678
|
+
* "A real logger call" means one whose receiver is PROVABLY the shipped
|
|
679
|
+
* Logger, not one that merely classifies. Classification accepts an
|
|
680
|
+
* unresolved `logger.info(M)` so its arguments still get checked, but
|
|
681
|
+
* exempting that same call from mutation analysis would trust it:
|
|
682
|
+
* `function f(logger) { const M = { up: 'safe' }; logger.info(M);
|
|
683
|
+
* Log.info(M.up) }` hands `M` to a function we know nothing about, which is
|
|
684
|
+
* free to rewrite `M.up` into PHI before the second call reads it.
|
|
685
|
+
*/
|
|
686
|
+
function callCanMutateArguments(context, node) {
|
|
687
|
+
if (isNonMutatingStatic(context, node)) return false;
|
|
688
|
+
return !isTrustedLoggerCall(context, node);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** An API method invoked on a receiver proven to be the shipped Logger. */
|
|
692
|
+
function isTrustedApiMethod(context, member) {
|
|
693
|
+
const method = staticPropertyName(context, member);
|
|
694
|
+
return (
|
|
695
|
+
method !== null &&
|
|
696
|
+
API_METHODS.has(method) &&
|
|
697
|
+
isTrustedLogger(context, member.object)
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function isTrustedLoggerCall(context, node) {
|
|
702
|
+
const callee = unwrap(node.callee);
|
|
703
|
+
if (!callee || callee.type !== 'MemberExpression') return false;
|
|
704
|
+
|
|
705
|
+
const method = staticPropertyName(context, callee);
|
|
706
|
+
if (method === null) return false;
|
|
707
|
+
|
|
708
|
+
// `Log.info.call(thisArg, 'm', M)` — the real method is one level in, but
|
|
709
|
+
// the method alone is not enough. `.call` runs that real body against a
|
|
710
|
+
// receiver of the caller's choosing, and the logger reaches its sinks
|
|
711
|
+
// through `this`, so a forged receiver
|
|
712
|
+
// (`Log.info.call({ logMessage(_m, o) { o.up = patientName } }, 'm', M)`)
|
|
713
|
+
// mutates M from inside a call that looks entirely legitimate. The `this`
|
|
714
|
+
// has to be the real Logger too.
|
|
715
|
+
if (method === 'call' || method === 'apply') {
|
|
716
|
+
const inner = unwrap(callee.object);
|
|
717
|
+
if (!inner || inner.type !== 'MemberExpression') return false;
|
|
718
|
+
if (!isTrustedApiMethod(context, inner)) return false;
|
|
719
|
+
const thisArg = node.arguments[0];
|
|
720
|
+
return (
|
|
721
|
+
!!thisArg &&
|
|
722
|
+
thisArg.type !== 'SpreadElement' &&
|
|
723
|
+
isTrustedLogger(context, thisArg)
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return isTrustedApiMethod(context, callee);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Does the binding leave this file's sight — written through, or handed to
|
|
732
|
+
* code that could write through it?
|
|
733
|
+
*
|
|
734
|
+
* `const` freezes the reference and nothing else. A message table patched
|
|
735
|
+
* after definition (`MESSAGES.startup = patientName`) reads as a table of
|
|
736
|
+
* literals at its declaration and leaks at the call site, and a table handed
|
|
737
|
+
* to `mutate(MESSAGES)` can be rewritten by a function we never see.
|
|
738
|
+
*
|
|
739
|
+
* Being read as a property, aliased to another const, or passed to one of the
|
|
740
|
+
* logger's own methods is not an escape — the last one matters, because
|
|
741
|
+
* handing a metadata object to `Log.info` is the entire point of having one.
|
|
742
|
+
*/
|
|
743
|
+
/**
|
|
744
|
+
* Guards the cycle `isMutable` → `callCanMutateArguments` → `describeCall` →
|
|
745
|
+
* `classifyReceiver` → `staticPropertyName` → `staticStringValue` →
|
|
746
|
+
* `isMutable`. Re-entering for the same binding means the question is already
|
|
747
|
+
* being answered further up the stack; assuming immutable there terminates
|
|
748
|
+
* without changing the outcome, because the outer frame still applies every
|
|
749
|
+
* check to that same binding.
|
|
750
|
+
*/
|
|
751
|
+
const mutabilityInProgress = new Set();
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Answers are cached per rule run. `isMutable` walks every reference of a
|
|
755
|
+
* binding, and resolution asks about the same binding once per call site, so
|
|
756
|
+
* an uncached message table referenced by R log calls costs O(R²). The cache
|
|
757
|
+
* is keyed on `context` as well as the variable because `loggerNames` is
|
|
758
|
+
* configurable per rule, and that changes what counts as a logger call.
|
|
759
|
+
*/
|
|
760
|
+
const mutabilityCache = new WeakMap();
|
|
761
|
+
|
|
762
|
+
function isMutable(context, variable) {
|
|
763
|
+
let perContext = mutabilityCache.get(context);
|
|
764
|
+
if (!perContext) {
|
|
765
|
+
perContext = new WeakMap();
|
|
766
|
+
mutabilityCache.set(context, perContext);
|
|
767
|
+
}
|
|
768
|
+
const cached = perContext.get(variable);
|
|
769
|
+
if (cached !== undefined) return cached;
|
|
770
|
+
|
|
771
|
+
if (mutabilityInProgress.has(variable)) return false;
|
|
772
|
+
mutabilityInProgress.add(variable);
|
|
773
|
+
let result;
|
|
774
|
+
try {
|
|
775
|
+
result = computeIsMutable(context, variable);
|
|
776
|
+
} finally {
|
|
777
|
+
mutabilityInProgress.delete(variable);
|
|
778
|
+
}
|
|
779
|
+
perContext.set(variable, result);
|
|
780
|
+
return result;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Every identifier a binding pattern introduces, however deeply nested. */
|
|
784
|
+
function collectPatternIdentifiers(node, out) {
|
|
785
|
+
if (!node) return;
|
|
786
|
+
switch (node.type) {
|
|
787
|
+
case 'Identifier':
|
|
788
|
+
out.push(node);
|
|
789
|
+
return;
|
|
790
|
+
case 'ObjectPattern':
|
|
791
|
+
for (const property of node.properties) {
|
|
792
|
+
collectPatternIdentifiers(
|
|
793
|
+
property.type === 'RestElement' ? property.argument : property.value,
|
|
794
|
+
out
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
return;
|
|
798
|
+
case 'ArrayPattern':
|
|
799
|
+
for (const element of node.elements) {
|
|
800
|
+
collectPatternIdentifiers(element, out);
|
|
801
|
+
}
|
|
802
|
+
return;
|
|
803
|
+
case 'AssignmentPattern':
|
|
804
|
+
collectPatternIdentifiers(node.left, out);
|
|
805
|
+
return;
|
|
806
|
+
case 'RestElement':
|
|
807
|
+
collectPatternIdentifiers(node.argument, out);
|
|
808
|
+
return;
|
|
809
|
+
default:
|
|
810
|
+
// A member expression as a destructuring target is a write, which
|
|
811
|
+
// `isWriteTarget` already reports.
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function bindingIdentifiersMutable(context, node) {
|
|
817
|
+
const identifiers = [];
|
|
818
|
+
collectPatternIdentifiers(node, identifiers);
|
|
819
|
+
return identifiers.some((identifier) => {
|
|
820
|
+
const variable = resolveVariable(context, identifier);
|
|
821
|
+
return !variable || isMutable(context, variable);
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** A value that is copied on read, so it carries no object identity. */
|
|
826
|
+
function isPrimitiveLiteral(node) {
|
|
827
|
+
const value = unwrap(node);
|
|
828
|
+
if (!value) return false;
|
|
829
|
+
if (value.type === 'Literal') return !value.regex;
|
|
830
|
+
return value.type === 'TemplateLiteral' && value.expressions.length === 0;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Destructuring reads the source, but a binding it extracts can still ALIAS
|
|
835
|
+
* part of it: `const { inner } = md; inner.k = patientName` rewrites `md`.
|
|
836
|
+
* So each extracted binding has to clear the same bar the source does.
|
|
837
|
+
*
|
|
838
|
+
* A binding that took a primitive is exempt, and that exemption is what keeps
|
|
839
|
+
* the rule usable. `const { up } = M` copies the string, so nothing done to
|
|
840
|
+
* `up` afterwards can reach `M` — without this, destructuring one scalar and
|
|
841
|
+
* passing it anywhere would make the whole table unresolvable, which is
|
|
842
|
+
* ordinary code rather than a leak.
|
|
843
|
+
*
|
|
844
|
+
* Anything not proven primitive stays conservative: rest elements, computed
|
|
845
|
+
* keys, defaults, nested patterns, and every key when the source literal
|
|
846
|
+
* itself could not be resolved.
|
|
847
|
+
*/
|
|
848
|
+
function patternBindingsMutable(context, pattern, sourceLiteral) {
|
|
849
|
+
for (const property of pattern.properties) {
|
|
850
|
+
if (property.type === 'RestElement') {
|
|
851
|
+
if (bindingIdentifiersMutable(context, property.argument)) return true;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const key = property.computed ? null : propertyKeyName(property);
|
|
855
|
+
const source =
|
|
856
|
+
key !== null && sourceLiteral
|
|
857
|
+
? objectProperty(context, sourceLiteral, key)
|
|
858
|
+
: null;
|
|
859
|
+
if (
|
|
860
|
+
source &&
|
|
861
|
+
property.value.type === 'Identifier' &&
|
|
862
|
+
isPrimitiveLiteral(source.value)
|
|
863
|
+
) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (bindingIdentifiersMutable(context, property.value)) return true;
|
|
867
|
+
}
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function computeIsMutable(context, variable) {
|
|
872
|
+
for (const reference of variable.references) {
|
|
873
|
+
if (reference.isWrite()) {
|
|
874
|
+
// A write that is not the declaration's own initializer is a rebind.
|
|
875
|
+
if (reference.init !== true) return true;
|
|
876
|
+
// The declaration's own binding occurrence says nothing about mutation,
|
|
877
|
+
// and it must not reach the escape analysis below: a destructured
|
|
878
|
+
// binding sits inside a `Property`, which that analysis reads as the
|
|
879
|
+
// very different `{ key: M }`.
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const identifier = reference.identifier;
|
|
884
|
+
const parent = identifier.parent;
|
|
885
|
+
if (!parent) continue;
|
|
886
|
+
|
|
887
|
+
if (parent.type === 'MemberExpression' && parent.object === identifier) {
|
|
888
|
+
// Climb the WHOLE chain before deciding. A write or a call anywhere
|
|
889
|
+
// along it reaches the root: `M.a.b = patientName` rewrites part of M,
|
|
890
|
+
// and `Log.constructor.prototype.newCorrelationId = …` replaces the
|
|
891
|
+
// generator while the reference's immediate parent is only
|
|
892
|
+
// `Log.constructor`.
|
|
893
|
+
let member = parent;
|
|
894
|
+
for (;;) {
|
|
895
|
+
if (isWriteTarget(member)) return true;
|
|
896
|
+
|
|
897
|
+
// `M.__defineGetter__('t', function () { this.up = patientName })`
|
|
898
|
+
// installs an accessor without anything that reads as a write, and
|
|
899
|
+
// `Object.prototype` hands that method to every object. So a call
|
|
900
|
+
// THROUGH the binding is a possible mutation.
|
|
901
|
+
//
|
|
902
|
+
// A proven Logger calling one of its OWN methods is the exception,
|
|
903
|
+
// and has to be: `Log.info('m', md)`, `Log.newCorrelationId()` and
|
|
904
|
+
// `scope.info('m', md)` are all calls through a binding, and treating
|
|
905
|
+
// them as mutations would make every Logger binding mutable, which
|
|
906
|
+
// would un-trust the logger and cascade into every rule.
|
|
907
|
+
//
|
|
908
|
+
// Both halves are load-bearing. Dropping the receiver check would
|
|
909
|
+
// exempt `attacker.info(M)`; dropping the method check would exempt
|
|
910
|
+
// `Log.__defineGetter__('newCorrelationId', …)`, which replaces the
|
|
911
|
+
// generator using a method the package never wrote.
|
|
912
|
+
const invocation = member.parent;
|
|
913
|
+
const invoked =
|
|
914
|
+
invocation &&
|
|
915
|
+
((invocation.type === 'CallExpression' &&
|
|
916
|
+
unwrap(invocation.callee) === member) ||
|
|
917
|
+
(invocation.type === 'NewExpression' &&
|
|
918
|
+
unwrap(invocation.callee) === member) ||
|
|
919
|
+
// `M.mutate`x`` calls M.mutate with `this === M` too.
|
|
920
|
+
(invocation.type === 'TaggedTemplateExpression' &&
|
|
921
|
+
unwrap(invocation.tag) === member));
|
|
922
|
+
if (invoked) {
|
|
923
|
+
// `Log.info.call(Log, …)` invokes at the second level, so unwrap
|
|
924
|
+
// the forwarding method to the real one before judging it.
|
|
925
|
+
let target = member;
|
|
926
|
+
let method = staticPropertyName(context, member);
|
|
927
|
+
if (method === 'call' || method === 'apply' || method === 'bind') {
|
|
928
|
+
const inner = unwrap(member.object);
|
|
929
|
+
if (inner && inner.type === 'MemberExpression') {
|
|
930
|
+
target = inner;
|
|
931
|
+
method = staticPropertyName(context, inner);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const ownApiCall =
|
|
935
|
+
method !== null &&
|
|
936
|
+
LOGGER_OWN_METHODS.has(method) &&
|
|
937
|
+
// The method has to hang off the binding itself. Otherwise
|
|
938
|
+
// `Log.constructor.prototype.info()` would ride the exemption on
|
|
939
|
+
// a name it only borrowed.
|
|
940
|
+
unwrap(target.object) === identifier &&
|
|
941
|
+
isTrustedLogger(context, identifier);
|
|
942
|
+
if (!ownApiCall) return true;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const next = member.parent;
|
|
946
|
+
if (
|
|
947
|
+
next &&
|
|
948
|
+
next.type === 'MemberExpression' &&
|
|
949
|
+
next.object === member
|
|
950
|
+
) {
|
|
951
|
+
member = next;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// `const A = M` — the alias points at the same object, so a write through
|
|
960
|
+
// A is a write through M. Following it beats treating aliasing as an
|
|
961
|
+
// escape, which would reject the ordinary `const md = base;` refactor.
|
|
962
|
+
if (parent.type === 'VariableDeclarator' && parent.init === identifier) {
|
|
963
|
+
if (parent.id.type === 'Identifier') {
|
|
964
|
+
const alias = resolveVariable(context, parent.id);
|
|
965
|
+
if (!alias || isMutable(context, alias)) return true;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
// `const { userId } = md` reads M rather than writing it — but each
|
|
969
|
+
// binding it extracts can alias a nested object, so they have to clear
|
|
970
|
+
// the same bar the whole-object alias above does.
|
|
971
|
+
if (parent.id.type === 'ObjectPattern') {
|
|
972
|
+
const literal = unwrapFreeze(context, immutableInit(variable));
|
|
973
|
+
const source =
|
|
974
|
+
literal && literal.type === 'ObjectExpression' ? literal : null;
|
|
975
|
+
if (patternBindingsMutable(context, parent.id, source)) return true;
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
// An ArrayPattern runs the iteration protocol, which is code.
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Handed to a function that could write through it.
|
|
983
|
+
if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
|
|
984
|
+
if (parent.callee === identifier) continue;
|
|
985
|
+
if (
|
|
986
|
+
parent.type === 'CallExpression' &&
|
|
987
|
+
!callCanMutateArguments(context, parent)
|
|
988
|
+
) {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Spread into another object, returned, put in an array — out of sight.
|
|
995
|
+
//
|
|
996
|
+
// `await M` belongs here too, and not obviously: awaiting a value calls
|
|
997
|
+
// its `then` with `this` bound to it, so a thenable metadata object
|
|
998
|
+
// rewrites itself at the await. `yield M` hands it to whoever drives the
|
|
999
|
+
// iterator.
|
|
1000
|
+
if (
|
|
1001
|
+
parent.type === 'SpreadElement' ||
|
|
1002
|
+
parent.type === 'ReturnStatement' ||
|
|
1003
|
+
parent.type === 'ArrayExpression' ||
|
|
1004
|
+
parent.type === 'Property' ||
|
|
1005
|
+
parent.type === 'AssignmentExpression' ||
|
|
1006
|
+
parent.type === 'AwaitExpression' ||
|
|
1007
|
+
parent.type === 'YieldExpression'
|
|
1008
|
+
) {
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/** Is this identifier the untouched global `Object`? */
|
|
1016
|
+
function isGlobalObject(context, node) {
|
|
1017
|
+
if (!node || node.type !== 'Identifier' || node.name !== 'Object') {
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
const variable = resolveVariable(context, node);
|
|
1021
|
+
// Absent, or present as a declared global with no definition of its own.
|
|
1022
|
+
return !variable || variable.defs.length === 0;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** `Object.freeze({...})` → the object literal, via the real `Object`. */
|
|
1026
|
+
function unwrapFreeze(context, node) {
|
|
1027
|
+
const call = unwrap(node);
|
|
1028
|
+
if (!call || call.type !== 'CallExpression') return call;
|
|
1029
|
+
const callee = unwrap(call.callee);
|
|
1030
|
+
if (
|
|
1031
|
+
callee &&
|
|
1032
|
+
callee.type === 'MemberExpression' &&
|
|
1033
|
+
staticPropertyName(context, callee) === 'freeze' &&
|
|
1034
|
+
isGlobalObject(context, unwrap(callee.object)) &&
|
|
1035
|
+
call.arguments.length === 1
|
|
1036
|
+
) {
|
|
1037
|
+
return unwrap(call.arguments[0]);
|
|
1038
|
+
}
|
|
1039
|
+
return call;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/* -------------------------------------------------------------------------
|
|
1043
|
+
* Value analysis
|
|
1044
|
+
* ---------------------------------------------------------------------- */
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Resolve an expression to an object literal, following `const` aliases and
|
|
1048
|
+
* `Object.freeze`. Returns null when it cannot be proven to be one —
|
|
1049
|
+
* including when the binding is mutable by the definition above.
|
|
1050
|
+
*/
|
|
1051
|
+
/**
|
|
1052
|
+
* Can reading this literal run any code?
|
|
1053
|
+
*
|
|
1054
|
+
* A table of constants is only constant if nothing inside it executes. Four
|
|
1055
|
+
* ways a literal smuggles in behaviour, all of which rewrite an approved
|
|
1056
|
+
* value before it is logged:
|
|
1057
|
+
*
|
|
1058
|
+
* - **A getter or setter.** Not a value but a callback, which the runtime
|
|
1059
|
+
* invokes while redacting, and which can rewrite its own siblings on the
|
|
1060
|
+
* way past: `{ up: 'safe', get t() { this.up = patientName } }`.
|
|
1061
|
+
* - **A shorthand method**, the same hazard reached through `toJSON`.
|
|
1062
|
+
* - **A function- or class-valued property.** `property.method` is false for
|
|
1063
|
+
* `{ mutate: function () { this.up = patientName } }`, but `M.mutate()`
|
|
1064
|
+
* rewrites `up` just the same, and nothing about that call reads as a write
|
|
1065
|
+
* to `M`.
|
|
1066
|
+
* - **`__proto__`.** It sets the prototype, so the getters and methods that
|
|
1067
|
+
* can run are ones this literal never mentions:
|
|
1068
|
+
* `{ __proto__: P, up: 'safe' }` inherits everything `P` defines.
|
|
1069
|
+
*
|
|
1070
|
+
* Nested literals are checked too — a getter one level down still runs, and
|
|
1071
|
+
* still reaches the outer object through a closure.
|
|
1072
|
+
*/
|
|
1073
|
+
function literalRunsCode(context, object, seen = new Set()) {
|
|
1074
|
+
if (seen.has(object)) return false;
|
|
1075
|
+
seen.add(object);
|
|
1076
|
+
return object.properties.some((property) => {
|
|
1077
|
+
if (property.type === 'SpreadElement') return false;
|
|
1078
|
+
if (property.kind !== 'init' || property.method) return true;
|
|
1079
|
+
if (!property.computed && propertyKeyName(property) === '__proto__') {
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
return valueRunsCode(context, property.value, seen);
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* The same question for a single value, at arbitrary depth.
|
|
1088
|
+
*
|
|
1089
|
+
* Split out because the hazard does not care how deeply it is nested or how
|
|
1090
|
+
* many names it is behind: `{ h: [[fn]] }` and
|
|
1091
|
+
* `const mutate = function () {…}; { mutate }` hide exactly what
|
|
1092
|
+
* `{ mutate() {…} }` states outright. Identifiers are followed through their
|
|
1093
|
+
* `const` initializers so an alias is not a way around the check.
|
|
1094
|
+
*
|
|
1095
|
+
* A call is NOT treated as executable. `{ userId: pub(value) }` is the
|
|
1096
|
+
* documented way to write metadata, and the call runs once while the literal
|
|
1097
|
+
* is built — before anything reads it — so it is not a callback the reader
|
|
1098
|
+
* can trigger.
|
|
1099
|
+
*/
|
|
1100
|
+
function valueRunsCode(context, node, seen = new Set()) {
|
|
1101
|
+
const value = unwrap(node);
|
|
1102
|
+
if (!value) return false;
|
|
1103
|
+
switch (value.type) {
|
|
1104
|
+
case 'FunctionExpression':
|
|
1105
|
+
case 'ArrowFunctionExpression':
|
|
1106
|
+
case 'ClassExpression':
|
|
1107
|
+
return true;
|
|
1108
|
+
case 'ObjectExpression':
|
|
1109
|
+
return literalRunsCode(context, value, seen);
|
|
1110
|
+
case 'ArrayExpression':
|
|
1111
|
+
return value.elements.some((element) =>
|
|
1112
|
+
valueRunsCode(context, element, seen)
|
|
1113
|
+
);
|
|
1114
|
+
case 'Identifier': {
|
|
1115
|
+
const variable = resolveVariable(context, value);
|
|
1116
|
+
if (!variable || seen.has(variable)) return false;
|
|
1117
|
+
seen.add(variable);
|
|
1118
|
+
return valueRunsCode(context, immutableInit(variable), seen);
|
|
1119
|
+
}
|
|
1120
|
+
default:
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function resolveObjectLiteral(context, node, seen = new Set()) {
|
|
1126
|
+
const current = unwrapFreeze(context, node);
|
|
1127
|
+
if (!current) return null;
|
|
1128
|
+
if (current.type === 'ObjectExpression') {
|
|
1129
|
+
return literalRunsCode(context, current) ? null : current;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (current.type === 'Identifier') {
|
|
1133
|
+
const variable = resolveVariable(context, current);
|
|
1134
|
+
if (!variable || seen.has(variable)) return null;
|
|
1135
|
+
seen.add(variable);
|
|
1136
|
+
if (isMutable(context, variable)) return null;
|
|
1137
|
+
return resolveObjectLiteral(context, immutableInit(variable), seen);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Look a property up in an object literal.
|
|
1145
|
+
*
|
|
1146
|
+
* A spread or a computed key anywhere poisons the whole lookup: both can
|
|
1147
|
+
* introduce or override the key we are about to approve, and neither is
|
|
1148
|
+
* visible to static analysis. `{ correlation: newCorrelationId(), [k]: id }`
|
|
1149
|
+
* is safe only if `k !== 'correlation'`, which we cannot know. Duplicate
|
|
1150
|
+
* literal keys resolve to the LAST, matching JavaScript.
|
|
1151
|
+
*
|
|
1152
|
+
* Anything executable ANYWHERE in the literal poisons it too, for the reason
|
|
1153
|
+
* given on {@link literalRunsCode}: reading one property can rewrite another,
|
|
1154
|
+
* so no property in such an object is a constant.
|
|
1155
|
+
*/
|
|
1156
|
+
/**
|
|
1157
|
+
* The same two lookups again, for RECEIVERS rather than values.
|
|
1158
|
+
*
|
|
1159
|
+
* Rule 1 and rule 2 want opposite things here. Asking "is this a constant"
|
|
1160
|
+
* of `const handlers = { emit: Log.info }` should say no — a call through
|
|
1161
|
+
* `handlers` could rewrite it. Asking "could `handlers.emit(msg)` be a logger
|
|
1162
|
+
* call" must still say yes, because answering no silences every rule at that
|
|
1163
|
+
* call site, which is the one outcome worse than a false positive.
|
|
1164
|
+
*
|
|
1165
|
+
* So receiver resolution follows the binding without the mutation gate and
|
|
1166
|
+
* without the executable-literal gate. It keeps the key-ambiguity checks: a
|
|
1167
|
+
* spread or computed key really can mean the method is not the one named.
|
|
1168
|
+
*/
|
|
1169
|
+
function resolveReceiverObject(context, node, seen = new Set()) {
|
|
1170
|
+
const current = unwrapFreeze(context, node);
|
|
1171
|
+
if (!current) return null;
|
|
1172
|
+
if (current.type === 'ObjectExpression') return current;
|
|
1173
|
+
if (current.type === 'Identifier') {
|
|
1174
|
+
const variable = resolveVariable(context, current);
|
|
1175
|
+
if (!variable || seen.has(variable)) return null;
|
|
1176
|
+
seen.add(variable);
|
|
1177
|
+
return resolveReceiverObject(context, bindingInit(variable), seen);
|
|
1178
|
+
}
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Every value the property `name` of this container could hold: whatever the
|
|
1184
|
+
* literal gives it, plus anything a later `container.name = …` assigns.
|
|
1185
|
+
*
|
|
1186
|
+
* Reading only the initializer is a bypass, because receiver resolution
|
|
1187
|
+
* deliberately ignores mutation:
|
|
1188
|
+
* `const handlers = { emit: analytics.info }; handlers.emit = Log.info`
|
|
1189
|
+
* resolves to the stale non-logger value and falls silent on a call that is
|
|
1190
|
+
* a logger call by the time it runs.
|
|
1191
|
+
*/
|
|
1192
|
+
function receiverPropertyCandidates(context, node, name, callNode) {
|
|
1193
|
+
const candidates = [];
|
|
1194
|
+
const property = receiverProperty(resolveReceiverObject(context, node), name);
|
|
1195
|
+
if (property) candidates.push(property.value);
|
|
1196
|
+
|
|
1197
|
+
const current = unwrap(node);
|
|
1198
|
+
if (!current || current.type !== 'Identifier') return candidates;
|
|
1199
|
+
const variable = resolveVariable(context, current);
|
|
1200
|
+
if (!variable) return candidates;
|
|
1201
|
+
|
|
1202
|
+
for (const reference of variable.references) {
|
|
1203
|
+
const identifier = reference.identifier;
|
|
1204
|
+
const member = identifier.parent;
|
|
1205
|
+
if (!member || member.type !== 'MemberExpression') {
|
|
1206
|
+
const call = member;
|
|
1207
|
+
if (
|
|
1208
|
+
!call ||
|
|
1209
|
+
call.type !== 'CallExpression' ||
|
|
1210
|
+
!call.arguments.includes(identifier) ||
|
|
1211
|
+
!reachesCall(call, callNode)
|
|
1212
|
+
) {
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// `Object.assign(handlers, { emit: Log.info })`, and the same thing one
|
|
1217
|
+
// refactor out as `Object.assign(handlers, methods)`, install the
|
|
1218
|
+
// method without ever naming `handlers.emit`.
|
|
1219
|
+
if (
|
|
1220
|
+
call.arguments[0] === identifier &&
|
|
1221
|
+
isNamespaceMethod(context, call, 'Object', 'assign')
|
|
1222
|
+
) {
|
|
1223
|
+
for (const source of call.arguments.slice(1)) {
|
|
1224
|
+
const match = receiverProperty(
|
|
1225
|
+
resolveReceiverObject(context, source),
|
|
1226
|
+
name
|
|
1227
|
+
);
|
|
1228
|
+
if (match) candidates.push(match.value);
|
|
1229
|
+
}
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// `function configure(target, fn) { target.emit = fn }` called as
|
|
1234
|
+
// `configure(handlers, Log.info)` installs a logger this analysis does
|
|
1235
|
+
// not follow, and DELIBERATELY so. Following it means reproducing
|
|
1236
|
+
// interprocedural dataflow inside a lint rule: parameter defaults,
|
|
1237
|
+
// destructuring at any depth, rest patterns, spread arguments,
|
|
1238
|
+
// reassignment, shadowing, and every way a function can be invoked.
|
|
1239
|
+
// Seven review rounds went into an implementation that kept finding new
|
|
1240
|
+
// spellings, and the last one had started reporting ordinary code —
|
|
1241
|
+
// `{ [key]: () => Log.info('setup') }` is a callback that logs a
|
|
1242
|
+
// literal, not a logger installed on `handlers`.
|
|
1243
|
+
//
|
|
1244
|
+
// A lint rule that cries wolf gets switched off, and a rule that is
|
|
1245
|
+
// switched off protects nothing.
|
|
1246
|
+
//
|
|
1247
|
+
// Being in the same file is NOT what makes a logger followable — the
|
|
1248
|
+
// local `configure` above is exactly as invisible as an imported one.
|
|
1249
|
+
// What is followed is a logger that reaches the call site without
|
|
1250
|
+
// passing through a call: a direct call, a `Log.scoped(...)` result, a
|
|
1251
|
+
// `const` alias of a method, a property assignment, an object literal,
|
|
1252
|
+
// or `Object.assign`. Note that a call site this misses is outside ALL
|
|
1253
|
+
// FOUR rules, metadata keys included. The runtime redacts metadata
|
|
1254
|
+
// VALUES; keys are only stopped by the catalog, which is mandatory
|
|
1255
|
+
// under privacyDefault('private') and absent by default otherwise.
|
|
1256
|
+
// The README states the split in those terms.
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
if (
|
|
1260
|
+
member.object !== identifier ||
|
|
1261
|
+
staticPropertyName(context, member) !== name
|
|
1262
|
+
) {
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
const assignment = member.parent;
|
|
1266
|
+
if (
|
|
1267
|
+
assignment &&
|
|
1268
|
+
assignment.type === 'AssignmentExpression' &&
|
|
1269
|
+
assignment.left === member &&
|
|
1270
|
+
reachesCall(assignment, callNode)
|
|
1271
|
+
) {
|
|
1272
|
+
candidates.push(assignment.right);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return candidates;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
|
|
1280
|
+
/** Is this a call to `Namespace.method(…)` on the untampered global? */
|
|
1281
|
+
function isNamespaceMethod(context, call, namespace, method) {
|
|
1282
|
+
const callee = unwrap(call.callee);
|
|
1283
|
+
return (
|
|
1284
|
+
!!callee &&
|
|
1285
|
+
callee.type === 'MemberExpression' &&
|
|
1286
|
+
builtinNamespaceOf(context, callee.object) === namespace &&
|
|
1287
|
+
staticPropertyName(context, callee) === method
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Could this write affect that call?
|
|
1293
|
+
*
|
|
1294
|
+
* A write earlier in the file can. So can one written later but inside a
|
|
1295
|
+
* function or a loop, which may run first or run again. A later write in
|
|
1296
|
+
* straight-line code cannot, and counting it would report the call before it
|
|
1297
|
+
* on evidence that does not exist yet.
|
|
1298
|
+
*/
|
|
1299
|
+
function reachesCall(write, callNode) {
|
|
1300
|
+
if (!callNode || write.range[0] < callNode.range[0]) return true;
|
|
1301
|
+
// A write inside a function or loop may run first, or run again.
|
|
1302
|
+
if (isDeferred(write)) return true;
|
|
1303
|
+
// And so may the CALL: in
|
|
1304
|
+
// `function run() { h.emit(phi) } h.emit = Log.info; run();`
|
|
1305
|
+
// the write is later in the file and still happens first. Source order only
|
|
1306
|
+
// settles the question when both sit in straight-line code.
|
|
1307
|
+
return isDeferred(callNode);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/** Is this node somewhere that can run out of source order, or more than once? */
|
|
1311
|
+
function isDeferred(node) {
|
|
1312
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
1313
|
+
switch (current.type) {
|
|
1314
|
+
case 'FunctionDeclaration':
|
|
1315
|
+
case 'FunctionExpression':
|
|
1316
|
+
case 'ArrowFunctionExpression':
|
|
1317
|
+
case 'ForStatement':
|
|
1318
|
+
case 'ForOfStatement':
|
|
1319
|
+
case 'ForInStatement':
|
|
1320
|
+
case 'WhileStatement':
|
|
1321
|
+
case 'DoWhileStatement':
|
|
1322
|
+
// An INSTANCE field initializer runs at construction, which can be long
|
|
1323
|
+
// after the line it is written on: `class C { leak = h.emit(phi) }`
|
|
1324
|
+
// runs when something says `new C()`. A static field runs immediately,
|
|
1325
|
+
// in source order, so it settles like any other straight-line code and
|
|
1326
|
+
// the walk continues past it.
|
|
1327
|
+
// eslint-disable-next-line no-fallthrough
|
|
1328
|
+
case 'PropertyDefinition':
|
|
1329
|
+
case 'FieldDefinition':
|
|
1330
|
+
case 'AccessorProperty':
|
|
1331
|
+
if (!current.static) return true;
|
|
1332
|
+
break;
|
|
1333
|
+
default:
|
|
1334
|
+
break;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return false;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function receiverProperty(object, name) {
|
|
1341
|
+
if (!object) return null;
|
|
1342
|
+
let match = null;
|
|
1343
|
+
for (const property of object.properties) {
|
|
1344
|
+
// Spread and computed keys stay whole-object ambiguity: either can be the
|
|
1345
|
+
// key we are looking for under a spelling we cannot read.
|
|
1346
|
+
if (property.type === 'SpreadElement') return null;
|
|
1347
|
+
if (property.computed) return null;
|
|
1348
|
+
if (propertyKeyName(property) !== name) continue;
|
|
1349
|
+
// Only the MATCHING property has to be a plain value. An unrelated
|
|
1350
|
+
// `helper() {}` next to `emit: Log.info` says nothing about what `emit`
|
|
1351
|
+
// is, and discarding the whole container over it silences every rule at
|
|
1352
|
+
// the call site.
|
|
1353
|
+
if (property.kind !== 'init' || property.method) return null;
|
|
1354
|
+
match = property;
|
|
1355
|
+
}
|
|
1356
|
+
return match;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function objectProperty(context, object, name) {
|
|
1360
|
+
if (!object) return null;
|
|
1361
|
+
if (literalRunsCode(context, object)) return null;
|
|
1362
|
+
let match = null;
|
|
1363
|
+
for (const property of object.properties) {
|
|
1364
|
+
if (property.type === 'SpreadElement') return null;
|
|
1365
|
+
if (property.computed) return null;
|
|
1366
|
+
if (propertyKeyName(property) !== name) continue;
|
|
1367
|
+
match = property;
|
|
1368
|
+
}
|
|
1369
|
+
return match;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* The compile-time value of a string expression, or null if it has none.
|
|
1374
|
+
*
|
|
1375
|
+
* Returns the string rather than a boolean so that a computed member access
|
|
1376
|
+
* (`Log[method]`) can be resolved to the name it actually reads.
|
|
1377
|
+
*/
|
|
1378
|
+
function staticStringValue(context, node, seen = new Set()) {
|
|
1379
|
+
const current = unwrap(node);
|
|
1380
|
+
if (!current) return null;
|
|
1381
|
+
switch (current.type) {
|
|
1382
|
+
case 'Literal':
|
|
1383
|
+
return typeof current.value === 'string' ? current.value : null;
|
|
1384
|
+
case 'TemplateLiteral':
|
|
1385
|
+
// A template with no `${}` is just a literal spelled differently.
|
|
1386
|
+
return current.expressions.length === 0
|
|
1387
|
+
? current.quasis[0].value.cooked
|
|
1388
|
+
: null;
|
|
1389
|
+
case 'Identifier': {
|
|
1390
|
+
const variable = resolveVariable(context, current);
|
|
1391
|
+
if (!variable || seen.has(variable)) return null;
|
|
1392
|
+
seen.add(variable);
|
|
1393
|
+
// No mutability check here, deliberately. Whatever this resolves to is
|
|
1394
|
+
// a string, and a string cannot be changed by anything an escape could
|
|
1395
|
+
// do to it — member writes are no-ops on a primitive, and `immutableInit`
|
|
1396
|
+
// already restricts this to `const`, which forbids rebinding. Escape
|
|
1397
|
+
// analysis belongs to the object paths, and `MemberExpression` below
|
|
1398
|
+
// still goes through it.
|
|
1399
|
+
return staticStringValue(context, immutableInit(variable), seen);
|
|
1400
|
+
}
|
|
1401
|
+
case 'MemberExpression': {
|
|
1402
|
+
// `MESSAGES.startup`, where MESSAGES is a const object of literals that
|
|
1403
|
+
// nothing writes through and nothing else can reach.
|
|
1404
|
+
const name = staticPropertyName(context, current, seen);
|
|
1405
|
+
if (name === null) return null;
|
|
1406
|
+
const object = resolveObjectLiteral(context, current.object, seen);
|
|
1407
|
+
const property = objectProperty(context, object, name);
|
|
1408
|
+
return property ? staticStringValue(context, property.value, seen) : null;
|
|
1409
|
+
}
|
|
1410
|
+
default:
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/** Is this expression a compile-time-constant string? */
|
|
1416
|
+
function isStaticString(context, node) {
|
|
1417
|
+
return staticStringValue(context, node) !== null;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* The property name of a member expression, however it is spelled: a dot, a
|
|
1422
|
+
* string subscript, or a subscript that resolves to a constant string.
|
|
1423
|
+
* `Log.info`, `Log['info']` and `const m = 'info'; Log[m]` are one thing.
|
|
1424
|
+
*/
|
|
1425
|
+
function staticPropertyName(context, node, seen = new Set()) {
|
|
1426
|
+
if (!node || node.type !== 'MemberExpression') return null;
|
|
1427
|
+
if (!node.computed) {
|
|
1428
|
+
// `this.#logger` is as much a named field as `this.logger`; the hash is
|
|
1429
|
+
// visibility, not identity.
|
|
1430
|
+
return node.property.type === 'Identifier' ||
|
|
1431
|
+
node.property.type === 'PrivateIdentifier'
|
|
1432
|
+
? node.property.name
|
|
1433
|
+
: null;
|
|
1434
|
+
}
|
|
1435
|
+
return staticStringValue(context, node.property, new Set(seen));
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* A message argument is acceptable when it is a static string, or a thunk
|
|
1440
|
+
* whose body is one. `() => \`patient ${id}\`` is exactly the dynamic case
|
|
1441
|
+
* the lazy-message API would otherwise smuggle past a naive check.
|
|
1442
|
+
*/
|
|
1443
|
+
function isStaticMessage(context, node) {
|
|
1444
|
+
const current = unwrap(node);
|
|
1445
|
+
if (!current) return false;
|
|
1446
|
+
if (isStaticString(context, current)) return true;
|
|
1447
|
+
if (
|
|
1448
|
+
current.type === 'ArrowFunctionExpression' ||
|
|
1449
|
+
current.type === 'FunctionExpression'
|
|
1450
|
+
) {
|
|
1451
|
+
if (current.body.type === 'BlockStatement') {
|
|
1452
|
+
const statements = current.body.body;
|
|
1453
|
+
if (statements.length !== 1 || statements[0].type !== 'ReturnStatement') {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
return isStaticString(context, statements[0].argument);
|
|
1457
|
+
}
|
|
1458
|
+
return isStaticString(context, current.body);
|
|
1459
|
+
}
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/* -------------------------------------------------------------------------
|
|
1464
|
+
* Receiver classification
|
|
1465
|
+
* ---------------------------------------------------------------------- */
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Classify a call's receiver: the Logger singleton, a ScopedLogger, or
|
|
1469
|
+
* something carrying a logger name that resolves to neither.
|
|
1470
|
+
*
|
|
1471
|
+
* Argument roles differ between the two shapes — `Log.info(msg, meta, sub)`
|
|
1472
|
+
* vs `scope.info(msg, meta)`, `Log.log(msg, options)` vs `scope.log(msg,
|
|
1473
|
+
* level, meta)` — so rules that key off position must know which they have.
|
|
1474
|
+
*
|
|
1475
|
+
* Resolution happens BEFORE the name check. `const logger = Log.scoped()` is
|
|
1476
|
+
* a ScopedLogger whatever it is called, and trusting the spelling first is
|
|
1477
|
+
* exactly how its metadata argument goes unexamined.
|
|
1478
|
+
*
|
|
1479
|
+
* The distinction between `'logger'` and `'ambiguous'` is provenance, not
|
|
1480
|
+
* confidence: an import of the singleton, or a bare global spelled with its
|
|
1481
|
+
* documented name, IS the Logger. A parameter or a property that merely
|
|
1482
|
+
* happens to be called `logger` could be anything, and correlation
|
|
1483
|
+
* provenance in particular must not trust it.
|
|
1484
|
+
*
|
|
1485
|
+
* @returns {'logger' | 'scoped' | 'ambiguous' | null}
|
|
1486
|
+
*/
|
|
1487
|
+
function classifyReceiver(context, node, seen = new Set()) {
|
|
1488
|
+
const current = unwrap(node);
|
|
1489
|
+
if (!current) return null;
|
|
1490
|
+
|
|
1491
|
+
if (current.type === 'Identifier') {
|
|
1492
|
+
const variable = resolveVariable(context, current);
|
|
1493
|
+
if (variable && !seen.has(variable)) {
|
|
1494
|
+
seen.add(variable);
|
|
1495
|
+
const def = singleDef(variable);
|
|
1496
|
+
|
|
1497
|
+
// `import { Log } from 'react-native-nitro-logger'`, possibly renamed.
|
|
1498
|
+
if (def && def.type === 'ImportBinding') {
|
|
1499
|
+
if (isLoggerImport(context, def)) return 'logger';
|
|
1500
|
+
// Same name, unknown module: still worth CHECKING its calls, never
|
|
1501
|
+
// worth TRUSTING as the singleton.
|
|
1502
|
+
const imported = def.node.imported?.name ?? def.node.local?.name;
|
|
1503
|
+
return loggerNames(context).has(imported) ? 'ambiguous' : null;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const init = bindingInit(variable);
|
|
1507
|
+
if (init) return classifyReceiver(context, init, seen);
|
|
1508
|
+
|
|
1509
|
+
// A parameter, or a binding we cannot pin down. The name is a hint,
|
|
1510
|
+
// never proof.
|
|
1511
|
+
return loggerNames(context).has(current.name) ? 'ambiguous' : null;
|
|
1512
|
+
}
|
|
1513
|
+
if (seen.has(variable)) return null;
|
|
1514
|
+
// Unresolved global. Only the canonical name is the singleton itself.
|
|
1515
|
+
if (current.name === canonicalLoggerName(context)) return 'logger';
|
|
1516
|
+
return loggerNames(context).has(current.name) ? 'ambiguous' : null;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// `this.logger.info(…)`, `deps.log.info(…)` — very common, and the property
|
|
1520
|
+
// name is the only signal available, so it can only ever be a hint.
|
|
1521
|
+
if (current.type === 'MemberExpression') {
|
|
1522
|
+
const name = staticPropertyName(context, current);
|
|
1523
|
+
return name !== null && loggerNames(context).has(name) ? 'ambiguous' : null;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// `Log.scoped(…)` and `scope.scoped(…)` both produce a ScopedLogger.
|
|
1527
|
+
if (current.type === 'CallExpression') {
|
|
1528
|
+
const callee = unwrap(current.callee);
|
|
1529
|
+
if (
|
|
1530
|
+
callee &&
|
|
1531
|
+
callee.type === 'MemberExpression' &&
|
|
1532
|
+
staticPropertyName(context, callee) === 'scoped' &&
|
|
1533
|
+
classifyReceiver(context, callee.object, seen) !== null
|
|
1534
|
+
) {
|
|
1535
|
+
return 'scoped';
|
|
1536
|
+
}
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/** Could this receiver be the Logger singleton? */
|
|
1544
|
+
function maybeLogger(receiver) {
|
|
1545
|
+
return receiver === 'logger' || receiver === 'ambiguous';
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Is this expression provably the Logger the package ships?
|
|
1550
|
+
*
|
|
1551
|
+
* Stricter than `classifyReceiver(...) === 'logger'`, and deliberately so.
|
|
1552
|
+
* Classification exists to decide what to CHECK, where a bare global named
|
|
1553
|
+
* `Log` is a fine hint. This exists to decide what to TRUST — currently only
|
|
1554
|
+
* `newCorrelationId`'s provenance — and a name that nothing in the file
|
|
1555
|
+
* defines proves nothing at all: `globalThis.Log = fakeLogger` satisfies it.
|
|
1556
|
+
* Only a binding traceable to an import of the real package counts.
|
|
1557
|
+
*/
|
|
1558
|
+
function isTrustedLogger(context, node, seen = new Set()) {
|
|
1559
|
+
const current = unwrap(node);
|
|
1560
|
+
if (!current) return false;
|
|
1561
|
+
|
|
1562
|
+
// `Log.scoped(id, 'net')` — a scope minted by a trusted Logger inherits its
|
|
1563
|
+
// trust, so `scope.info(M)` stays exempt from mutation analysis.
|
|
1564
|
+
if (current.type === 'CallExpression') {
|
|
1565
|
+
const callee = unwrap(current.callee);
|
|
1566
|
+
return (
|
|
1567
|
+
!!callee &&
|
|
1568
|
+
callee.type === 'MemberExpression' &&
|
|
1569
|
+
staticPropertyName(context, callee) === 'scoped' &&
|
|
1570
|
+
isTrustedLogger(context, callee.object, seen)
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (current.type !== 'Identifier') return false;
|
|
1575
|
+
const variable = resolveVariable(context, current);
|
|
1576
|
+
if (!variable || seen.has(variable)) return false;
|
|
1577
|
+
seen.add(variable);
|
|
1578
|
+
|
|
1579
|
+
// The binding has to still hold what it was given. A rebind
|
|
1580
|
+
// (`let L = Log; L = fakeLogger`), a write through it
|
|
1581
|
+
// (`Log.newCorrelationId = () => patient.mrn`), or an escape into code we
|
|
1582
|
+
// cannot see all mean the method we are about to trust may not be the one
|
|
1583
|
+
// the package shipped.
|
|
1584
|
+
if (isMutable(context, variable)) return false;
|
|
1585
|
+
|
|
1586
|
+
const def = singleDef(variable);
|
|
1587
|
+
if (def && def.type === 'ImportBinding') return isLoggerImport(context, def);
|
|
1588
|
+
// `const` only: `immutableInit` rather than `bindingInit`, because the
|
|
1589
|
+
// latter reports a declaration initializer that a later write has replaced.
|
|
1590
|
+
return isTrustedLogger(context, immutableInit(variable), seen);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/* -------------------------------------------------------------------------
|
|
1594
|
+
* Call description
|
|
1595
|
+
* ---------------------------------------------------------------------- */
|
|
1596
|
+
|
|
1597
|
+
function hasSpread(args) {
|
|
1598
|
+
return args.some((a) => a.type === 'SpreadElement');
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/** `Log.info`, `Log['info']`, `Log.info.bind(Log)` → a described method. */
|
|
1602
|
+
function describeMethodReference(context, node, args) {
|
|
1603
|
+
const current = unwrap(node);
|
|
1604
|
+
if (!current) return null;
|
|
1605
|
+
|
|
1606
|
+
if (current.type === 'MemberExpression') {
|
|
1607
|
+
const method = staticPropertyName(context, current);
|
|
1608
|
+
if (method === null || !API_METHODS.has(method)) return null;
|
|
1609
|
+
const receiver = classifyReceiver(context, current.object);
|
|
1610
|
+
if (receiver === null) return null;
|
|
1611
|
+
return { method, receiver, args, spreadArgs: hasSpread(args) };
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// A partially applied bind shifts every position, so it is reported as
|
|
1615
|
+
// unanalyzable rather than trusted.
|
|
1616
|
+
if (current.type === 'CallExpression') {
|
|
1617
|
+
const callee = unwrap(current.callee);
|
|
1618
|
+
if (
|
|
1619
|
+
callee &&
|
|
1620
|
+
callee.type === 'MemberExpression' &&
|
|
1621
|
+
staticPropertyName(context, callee) === 'bind'
|
|
1622
|
+
) {
|
|
1623
|
+
const inner = describeMethodReference(context, callee.object, args);
|
|
1624
|
+
if (!inner) return null;
|
|
1625
|
+
return {
|
|
1626
|
+
...inner,
|
|
1627
|
+
spreadArgs: inner.spreadArgs || current.arguments.length > 1,
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* Describe any call into the logger's public API — level helpers, `log`,
|
|
1637
|
+
* `scoped`, `subsystem`, `resetSubsystem` — normalizing away
|
|
1638
|
+
* `.call`/`.apply`/`.bind`, computed access, destructured methods and method
|
|
1639
|
+
* aliases so every rule sees one shape.
|
|
1640
|
+
*
|
|
1641
|
+
* Returns `{ method, receiver, args, spreadArgs }` where `args` are the
|
|
1642
|
+
* logical arguments with any `thisArg` stripped, or null if this is not an
|
|
1643
|
+
* API call. `spreadArgs` means the call exists but its arguments cannot be
|
|
1644
|
+
* matched to positions — which is a finding, not a reason to stay quiet.
|
|
1645
|
+
*/
|
|
1646
|
+
function describeCall(context, node) {
|
|
1647
|
+
const callee = unwrap(node.callee);
|
|
1648
|
+
if (!callee) return null;
|
|
1649
|
+
|
|
1650
|
+
if (callee.type === 'MemberExpression') {
|
|
1651
|
+
// `Log.info.call(thisArg, message, …)` / `.apply(thisArg, argsArray)`
|
|
1652
|
+
const outer = staticPropertyName(context, callee);
|
|
1653
|
+
if (outer === 'call' || outer === 'apply') {
|
|
1654
|
+
const inner = unwrap(callee.object);
|
|
1655
|
+
if (inner && inner.type === 'MemberExpression') {
|
|
1656
|
+
const method = staticPropertyName(context, inner);
|
|
1657
|
+
if (method !== null && API_METHODS.has(method)) {
|
|
1658
|
+
const receiver = classifyReceiver(context, inner.object);
|
|
1659
|
+
if (receiver === null) return null;
|
|
1660
|
+
const args = outer === 'call' ? node.arguments.slice(1) : [];
|
|
1661
|
+
return {
|
|
1662
|
+
method,
|
|
1663
|
+
receiver,
|
|
1664
|
+
// `.apply` passes an array — positional analysis is meaningless,
|
|
1665
|
+
// so report only that the call exists.
|
|
1666
|
+
args,
|
|
1667
|
+
spreadArgs: outer === 'apply' || hasSpread(args),
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
const method = staticPropertyName(context, callee);
|
|
1674
|
+
const receiver = classifyReceiver(context, callee.object);
|
|
1675
|
+
|
|
1676
|
+
if (method !== null && API_METHODS.has(method)) {
|
|
1677
|
+
if (receiver === null) return null;
|
|
1678
|
+
return {
|
|
1679
|
+
method,
|
|
1680
|
+
receiver,
|
|
1681
|
+
args: node.arguments,
|
|
1682
|
+
spreadArgs: hasSpread(node.arguments),
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// `Log['in' + 'fo'](…)` / `Log[pick()](…)` — the receiver is a logger and
|
|
1687
|
+
// the method is unreadable, so we cannot rule out that this is a log
|
|
1688
|
+
// call. That is a finding, not a reason to fall silent.
|
|
1689
|
+
if (method === null && callee.computed && receiver !== null) {
|
|
1690
|
+
return {
|
|
1691
|
+
method: null,
|
|
1692
|
+
receiver,
|
|
1693
|
+
args: node.arguments,
|
|
1694
|
+
spreadArgs: true,
|
|
1695
|
+
opaqueMethod: true,
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// `const handlers = { emit: Log.info }; handlers.emit(msg)` — the method
|
|
1700
|
+
// lives in a const object rather than a binding of its own.
|
|
1701
|
+
if (method !== null) {
|
|
1702
|
+
const candidates = receiverPropertyCandidates(
|
|
1703
|
+
context,
|
|
1704
|
+
callee.object,
|
|
1705
|
+
method,
|
|
1706
|
+
node
|
|
1707
|
+
);
|
|
1708
|
+
const described = candidates
|
|
1709
|
+
.map((value) => describeMethodReference(context, value, node.arguments))
|
|
1710
|
+
.filter(Boolean);
|
|
1711
|
+
// Exactly one possible value, and it is a logger method: analyze it.
|
|
1712
|
+
if (candidates.length === 1 && described.length === 1)
|
|
1713
|
+
return described[0];
|
|
1714
|
+
// Several possible values and at least one is a logger method. Which
|
|
1715
|
+
// one runs is unknowable, so this is a finding rather than a guess.
|
|
1716
|
+
if (described.length > 0) {
|
|
1717
|
+
return {
|
|
1718
|
+
method: null,
|
|
1719
|
+
receiver: described[0].receiver,
|
|
1720
|
+
args: node.arguments,
|
|
1721
|
+
spreadArgs: true,
|
|
1722
|
+
opaqueMethod: true,
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return null;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// `(Log.info.bind(Log))(msg)` — the callee is the bind call itself.
|
|
1730
|
+
if (callee.type === 'CallExpression') {
|
|
1731
|
+
return describeMethodReference(context, callee, node.arguments);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
if (callee.type !== 'Identifier') return null;
|
|
1735
|
+
|
|
1736
|
+
const variable = resolveVariable(context, callee);
|
|
1737
|
+
const def = singleDef(variable);
|
|
1738
|
+
if (!def || def.type !== 'Variable') return null;
|
|
1739
|
+
|
|
1740
|
+
const pattern = def.node.id;
|
|
1741
|
+
|
|
1742
|
+
// `const { info } = Log` / `{ info: emit }` / `{ info = fallback }`
|
|
1743
|
+
if (pattern.type === 'ObjectPattern') {
|
|
1744
|
+
const property = pattern.properties.find((p) => {
|
|
1745
|
+
if (p.type !== 'Property') return false;
|
|
1746
|
+
const target =
|
|
1747
|
+
p.value.type === 'AssignmentPattern' ? p.value.left : p.value;
|
|
1748
|
+
return target === def.name;
|
|
1749
|
+
});
|
|
1750
|
+
if (!property) return null;
|
|
1751
|
+
const method = propertyKeyName(property);
|
|
1752
|
+
if (method === null || !API_METHODS.has(method)) return null;
|
|
1753
|
+
const receiver = classifyReceiver(context, def.node.init);
|
|
1754
|
+
if (receiver === null) return null;
|
|
1755
|
+
return {
|
|
1756
|
+
method,
|
|
1757
|
+
receiver,
|
|
1758
|
+
args: node.arguments,
|
|
1759
|
+
spreadArgs: hasSpread(node.arguments),
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// `const emit = Log.info` and `const [emit] = [Log.info]`, plus the `.bind`
|
|
1764
|
+
// form of either.
|
|
1765
|
+
return describeMethodReference(
|
|
1766
|
+
context,
|
|
1767
|
+
bindingInit(variable),
|
|
1768
|
+
node.arguments
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* An API call restricted to the message-emitting methods — plus calls whose
|
|
1774
|
+
* method name could not be read at all, since one of the things they could be
|
|
1775
|
+
* is a log call.
|
|
1776
|
+
*/
|
|
1777
|
+
function describeLogCall(context, node) {
|
|
1778
|
+
const call = describeCall(context, node);
|
|
1779
|
+
if (!call) return null;
|
|
1780
|
+
return LOG_METHODS.has(call.method) || call.opaqueMethod ? call : null;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/** `Log.scoped(correlation, subsystem, metadata)` or `scope.scoped(…)`. */
|
|
1784
|
+
function describeScopedCall(context, node) {
|
|
1785
|
+
const call = describeCall(context, node);
|
|
1786
|
+
return call && call.method === 'scoped' ? call : null;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/** `Log.subsystem(name, level)` / `Log.resetSubsystem(name)`. */
|
|
1790
|
+
function describeSubsystemConfigCall(context, node) {
|
|
1791
|
+
const call = describeCall(context, node);
|
|
1792
|
+
if (!call || !CONFIG_METHODS.has(call.method)) return null;
|
|
1793
|
+
// ScopedLogger has no level-configuration API.
|
|
1794
|
+
return maybeLogger(call.receiver) ? call : null;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/* -------------------------------------------------------------------------
|
|
1798
|
+
* Argument-position modeling
|
|
1799
|
+
* ---------------------------------------------------------------------- */
|
|
1800
|
+
|
|
1801
|
+
const ABSENT = { kind: 'absent' };
|
|
1802
|
+
const found = (node) => ({ kind: 'value', node });
|
|
1803
|
+
const opaque = (node) => ({ kind: 'unanalyzable', node });
|
|
1804
|
+
|
|
1805
|
+
/** Nothing passed in this position, or an explicit "no value". */
|
|
1806
|
+
function isOmitted(context, node) {
|
|
1807
|
+
const current = unwrap(node);
|
|
1808
|
+
if (!current) return true;
|
|
1809
|
+
if (current.type === 'Literal' && current.value === null) return true;
|
|
1810
|
+
if (current.type === 'Identifier' && current.name === 'undefined') {
|
|
1811
|
+
// Only the real global; a local named `undefined` is someone's problem.
|
|
1812
|
+
const variable = resolveVariable(context, current);
|
|
1813
|
+
return !variable || variable.defs.length === 0;
|
|
1814
|
+
}
|
|
1815
|
+
return false;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* A named property of an options object, following `const` aliases.
|
|
1820
|
+
*
|
|
1821
|
+
* @returns {{kind:'value',node}|{kind:'absent'}|{kind:'unanalyzable',node}}
|
|
1822
|
+
*/
|
|
1823
|
+
function optionsProperty(context, node, name) {
|
|
1824
|
+
const current = unwrap(node);
|
|
1825
|
+
if (isOmitted(context, current)) return ABSENT;
|
|
1826
|
+
const object = resolveObjectLiteral(context, current);
|
|
1827
|
+
if (!object) return opaque(current);
|
|
1828
|
+
// A spread or a computed key can introduce or override this field under a
|
|
1829
|
+
// spelling we cannot read, so neither can be reasoned past.
|
|
1830
|
+
const wildcard = object.properties.find(
|
|
1831
|
+
(p) => p.type === 'SpreadElement' || p.computed
|
|
1832
|
+
);
|
|
1833
|
+
if (wildcard) return opaque(wildcard.key ?? wildcard);
|
|
1834
|
+
const property = objectProperty(context, object, name);
|
|
1835
|
+
if (!property) return ABSENT;
|
|
1836
|
+
// `{ subsystem: undefined }` states the field is absent, which is what the
|
|
1837
|
+
// runtime sees too.
|
|
1838
|
+
return isOmitted(context, property.value) ? ABSENT : found(property.value);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* `Log.log(msg, options)` and `scope.log(msg, level, meta)` share a name.
|
|
1843
|
+
* When the receiver is ambiguous the second argument decides: a bare level
|
|
1844
|
+
* string means the ScopedLogger shape, an object literal means the Logger
|
|
1845
|
+
* shape, and anything else — including an omitted argument, since a scope's
|
|
1846
|
+
* level is defaulted — means both have to be checked.
|
|
1847
|
+
*/
|
|
1848
|
+
function logCallShape({ receiver, args, method }, context) {
|
|
1849
|
+
// `logMessage` exists only on the Logger and takes exactly one shape,
|
|
1850
|
+
// `(message, options)`. There is no ScopedLogger overload to disambiguate
|
|
1851
|
+
// against, so it never needs the argument sniffing below.
|
|
1852
|
+
if (method === 'logMessage') return 'logger';
|
|
1853
|
+
if (receiver === 'scoped') return 'scoped';
|
|
1854
|
+
if (receiver === 'logger') return 'logger';
|
|
1855
|
+
const second = unwrap(args[1]);
|
|
1856
|
+
const level = staticStringValue(context, second);
|
|
1857
|
+
if (level !== null && LEVEL_METHODS.has(level)) return 'scoped';
|
|
1858
|
+
if (second && second.type === 'ObjectExpression') return 'logger';
|
|
1859
|
+
return 'both';
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
* Every argument position carrying metadata for this call.
|
|
1864
|
+
*
|
|
1865
|
+
* `Log.<level>(message, metadata, subsystem)` · `scope.<level>(message,
|
|
1866
|
+
* metadata)` · `scope.log(message, level, metadata)` · `Log.log(message,
|
|
1867
|
+
* { metadata })`.
|
|
1868
|
+
*/
|
|
1869
|
+
function metadataArguments(context, call) {
|
|
1870
|
+
// Spread is handled by the rule, which has the call node to report on.
|
|
1871
|
+
if (call.spreadArgs) return [];
|
|
1872
|
+
const { method, args } = call;
|
|
1873
|
+
if (!OPTIONS_METHODS.has(method)) {
|
|
1874
|
+
return isOmitted(context, args[1]) ? [] : [found(args[1])];
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
const shape = logCallShape(call, context);
|
|
1878
|
+
const results = [];
|
|
1879
|
+
if (shape === 'scoped' || shape === 'both') {
|
|
1880
|
+
if (!isOmitted(context, args[2])) results.push(found(args[2]));
|
|
1881
|
+
}
|
|
1882
|
+
if (shape === 'logger' || shape === 'both') {
|
|
1883
|
+
for (const field of METADATA_OPTION_FIELDS) {
|
|
1884
|
+
const property = optionsProperty(context, args[1], field);
|
|
1885
|
+
if (property.kind !== 'absent') results.push(property);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return results;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/** Positions carrying an options-object field of the Logger shape. */
|
|
1892
|
+
function loggerOptionField(context, call, name) {
|
|
1893
|
+
if (!OPTIONS_METHODS.has(call.method)) return [];
|
|
1894
|
+
const shape = logCallShape(call, context);
|
|
1895
|
+
if (shape === 'scoped') return [];
|
|
1896
|
+
const property = optionsProperty(context, call.args[1], name);
|
|
1897
|
+
return property.kind === 'absent' ? [] : [property];
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
/** Argument positions carrying a subsystem name. */
|
|
1901
|
+
function subsystemArguments(context, call) {
|
|
1902
|
+
const { method, receiver, args } = call;
|
|
1903
|
+
if (!OPTIONS_METHODS.has(method)) {
|
|
1904
|
+
// Only the Logger's level helpers take a subsystem third argument;
|
|
1905
|
+
// ScopedLogger inherits it and has no such parameter.
|
|
1906
|
+
if (!maybeLogger(receiver)) return [];
|
|
1907
|
+
if (call.spreadArgs) return [];
|
|
1908
|
+
return isOmitted(context, args[2]) ? [] : [found(args[2])];
|
|
1909
|
+
}
|
|
1910
|
+
if (call.spreadArgs) return [];
|
|
1911
|
+
return loggerOptionField(context, call, 'subsystem');
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/** Argument positions carrying a correlation ID. */
|
|
1915
|
+
function correlationArguments(context, call) {
|
|
1916
|
+
if (call.spreadArgs) return [];
|
|
1917
|
+
return loggerOptionField(context, call, 'correlation');
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
module.exports = {
|
|
1921
|
+
API_METHODS,
|
|
1922
|
+
CONFIG_METHODS,
|
|
1923
|
+
LEVEL_METHODS,
|
|
1924
|
+
LOGGER_OWN_METHODS,
|
|
1925
|
+
LOG_METHODS,
|
|
1926
|
+
classifyReceiver,
|
|
1927
|
+
correlationArguments,
|
|
1928
|
+
describeCall,
|
|
1929
|
+
describeLogCall,
|
|
1930
|
+
describeScopedCall,
|
|
1931
|
+
describeSubsystemConfigCall,
|
|
1932
|
+
isMutable,
|
|
1933
|
+
isOmitted,
|
|
1934
|
+
isStaticMessage,
|
|
1935
|
+
isStaticString,
|
|
1936
|
+
isTrustedLogger,
|
|
1937
|
+
maybeLogger,
|
|
1938
|
+
metadataArguments,
|
|
1939
|
+
objectProperty,
|
|
1940
|
+
optionsProperty,
|
|
1941
|
+
resolveObjectLiteral,
|
|
1942
|
+
resolveVariable,
|
|
1943
|
+
singleDef,
|
|
1944
|
+
staticPropertyName,
|
|
1945
|
+
staticStringValue,
|
|
1946
|
+
subsystemArguments,
|
|
1947
|
+
unwrap,
|
|
1948
|
+
};
|