Skip to content

script-utils/linters/over-exposure

Over-exposure analyzer.

Finds declarations exposed more broadly than their references require, so the exposure can be tightened:

  • exported symbols referenced only within their own file — the export can be dropped. - public class members referenced only inside their own class — they can be private. - public class members referenced only inside their own class and its subclasses — they can be protected. - protected class members referenced only inside their own class — they can be private.

The analysis is whole-program and type-aware: it uses the TypeScript language service’s find-all-references to locate every use of each declaration, then classifies the tightest exposure that still covers all references. Because findReferences does not link a string-literal argument back to a member declaration when the parameter is a conditional/mapped key type (e.g. bind(component, 'someSetting') where the key is typed ConditionalKeys<Settings, …>), member analysis additionally treats a string literal whose text equals the member name as a reference — but only when its contextual type is a string-literal type (or union of them) all of whose members are keys of the declaring class, so incidental same-named strings (console.log('name')) are not miscounted. Members invoked by the framework via registration rather than by visible references (Obsidian lifecycle hooks, settings-tab display, etc.) are excluded via LIFECYCLE_ALLOWLIST, as are override and static members whose exposure is constrained by a base declaration. ECMAScript hard-private members (#name) are also excluded — they are already maximally private and cannot carry a visibility modifier. A declaration carrying a TSDoc (/** … *\/) documentation comment is likewise excluded — whether a top-level export or a class member, documenting it is a deliberate signal that it is part of the intended public API, regardless of where it currently happens to be referenced.

A member referenced only from test files is reported with OverExposureFinding.isForcedByTestOnly set, surfacing members widened purely for testability — the canonical case for extracting logic into an independently testable component instead.

Beyond reporting, passing shouldFix to findOverExposure / analyzeOverExposure rewrites the source in place: dropping the export keyword, or replacing/inserting a private or protected modifier. Each finding then carries OverExposureFinding.wasFixed / OverExposureFinding.skipReason. Changes that cannot be safely automated — those forced only by test references, decorated members, or an export shared with a still-exported sibling — are reported as skipped and left untouched.

Additionally passing shouldForce (only meaningful together with shouldFix) tightens the declarations held wide purely by test references too — the production half of the canonical test-only migration: the member becomes private / the export is dropped, leaving only the test to switch to a cast-based access. Decorated members and exports shared with a still-exported sibling stay skipped even under shouldForce, because no edit can be applied safely.

InterfaceDescription
AnalyzeOverExposureParamsParameters for analyzeOverExposure.
CreateLanguageServiceHostParamsParameters for createLanguageServiceHost.
CreateProjectLanguageServiceParamsParameters for createProjectLanguageService.
FindOverExposureParamsParameters for findOverExposure.
FormatOverExposureFindingsOptionsOptions for formatOverExposureFindings.
OverExposureFileSystemThe subset of file-system operations the language-service host needs. Injecting this (rather than always using typescript’s sys) lets tests drive the analyzer against in-memory sources with no real file-system access.
OverExposureFindingA single over-exposed declaration.
OverExposureProgressProgress reported while analyzing a project, one event per analyzed source file.
FunctionDescription
analyzeOverExposureAnalyzes a project (already loaded into a language service) for over-exposed declarations, and — when AnalyzeOverExposureParams.shouldFix is set — tightens every fixable finding in place via AnalyzeOverExposureParams.writeFile. Findings that cannot be safely automated (exposed only for tests, decorated, or sharing an export keyword with a still-exported sibling) carry a OverExposureFinding.skipReason and are left untouched — unless AnalyzeOverExposureParams.shouldForce is set, which additionally tightens the test-only findings (decorated and shared-export ones still stay skipped).
createLanguageServiceHostBuilds a LanguageServiceHost backed by an explicit file list and file system.
createProjectLanguageServiceBuilds a language service over every file in a project’s tsconfig.json.
findOverExposureFinds over-exposed declarations in a project on disk. When FindOverExposureParams.shouldFix is set, each fixable finding is also tightened in place (rewriting the affected source files via typescript’s sys.writeFile).
formatOverExposureFindingsFormats over-exposure findings as a human-readable report, using the grouped layout of ESLint’s stylish formatter. Findings are grouped by file and sorted by line: each file is rendered as a path header line, followed by one indented line:column change -- reason row per finding (the line:column is right-padded so the rows align within a file). Terminals such as VS Code render each indented line:column as a clickable link — resolved against the file header above it — that jumps straight to the declaration. File groups are separated by a blank line. In a fix run (findings carrying OverExposureFinding.wasFixed / OverExposureFinding.skipReason) each change row is suffixed with [fixed] or [skipped: …], and the trailing summary appends the fixed/skipped counts.
TypeDescription
CurrentExposureThe exposure level a declaration currently has.
OverExposureSkipReasonWhy a fix run left a finding untouched: - decorated — the member carries a decorator, so the modifier insertion point is ambiguous. - shared-export — the export keyword is shared with a still-exported sibling declarator. - test-only — the declaration is exposed purely for tests, so tightening would break the test.
SuggestedExposureThe (tighter) exposure level a declaration could be reduced to.
VariableDescription
LIFECYCLE_ALLOWLISTLifecycle and framework members invoked by Obsidian or a base class via registration rather than by references the language service can see. Tightening these would break the framework contract, so they are never reported.