mirror of
https://github.com/selfxyz/self.git
synced 2026-01-06 21:34:13 -05:00
Add docstring reporting script and workflows (#1333)
* Trim docstring coverage snapshots * format all the tings * update lock * Update docstring coverage snapshots (#1521) * docstring fixes * address agent feedback * update lock files * address agent feedback * lock react-native-svg version to prevent pipeline failures * update docstring logic * remove docstring coverage from ci * remove old report, fix cursorignroe rule
This commit is contained in:
@@ -136,8 +136,9 @@ app/android/android-passport-nfc-reader/app/src/main/assets/tessdata/
|
||||
# Development & Testing
|
||||
# ========================================
|
||||
|
||||
# Test coverage
|
||||
# Test coverage (but allow docs/coverage for docstring reports)
|
||||
**/coverage/
|
||||
!docs/coverage/
|
||||
**/.nyc_output/
|
||||
|
||||
# Test files (optional - you might want AI to see tests)
|
||||
@@ -261,6 +262,9 @@ circuits/ptau/
|
||||
!metro.config.*
|
||||
!tamagui.config.ts
|
||||
|
||||
# Allow docstring coverage reports (tracked in git for coverage tracking)
|
||||
!docs/coverage/*.json
|
||||
|
||||
# Ensure source code is accessible
|
||||
!**/*.ts
|
||||
!**/*.tsx
|
||||
|
||||
@@ -22,8 +22,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1194.0)
|
||||
aws-sdk-core (3.239.2)
|
||||
aws-partitions (1.1198.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -34,7 +34,7 @@ GEM
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.206.0)
|
||||
aws-sdk-s3 (1.209.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -43,7 +43,7 @@ GEM
|
||||
babosa (1.0.4)
|
||||
base64 (0.3.0)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (3.3.1)
|
||||
bigdecimal (4.0.1)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.16.2)
|
||||
addressable (~> 2.8)
|
||||
@@ -118,7 +118,7 @@ GEM
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.1)
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
@@ -219,7 +219,7 @@ GEM
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
i18n (1.14.7)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.18.0)
|
||||
@@ -229,7 +229,8 @@ GEM
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.27.0)
|
||||
minitest (6.0.0)
|
||||
prism (~> 1.5)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.18.0)
|
||||
multipart-post (2.4.1)
|
||||
@@ -244,6 +245,7 @@ GEM
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
prism (1.7.0)
|
||||
public_suffix (4.0.7)
|
||||
racc (1.8.1)
|
||||
rake (13.3.1)
|
||||
|
||||
14
app/docs/DOCSTRING_STYLE_GUIDE.md
Normal file
14
app/docs/DOCSTRING_STYLE_GUIDE.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Mobile app docstring style guide
|
||||
|
||||
Docstrings for the React Native app live alongside the source in `app/src`. We follow [TSDoc](https://tsdoc.org) conventions so that typed tooling can generate consistent API documentation.
|
||||
|
||||
## Authoring guidelines
|
||||
|
||||
- Document every exported component, hook, utility, or type alias with a leading `/** ... */` block written in the imperative mood.
|
||||
- Include `@param`, `@returns`, and `@remarks` tags when they improve clarity, especially for side-effects or platform-specific behaviour.
|
||||
- Keep examples concise. Prefer inline code blocks for short snippets and use fenced blocks only when you need multiple lines.
|
||||
- Mention platform differences explicitly (for example, “iOS only”) so consumers understand the scope of the implementation.
|
||||
|
||||
## Coverage expectations
|
||||
|
||||
Docstring coverage can be checked locally by running `yarn docstrings:app` (or `yarn docstrings` for both app and SDK). The reports generate JSON snapshots in `docs/coverage/*.json` that can be committed to track progress over time. Coverage targets are not enforced—treat the reports as guardrails to identify documentation gaps.
|
||||
@@ -2131,7 +2131,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- Sentry/HybridSDK (= 8.53.2)
|
||||
- Yoga
|
||||
- RNSVG (15.15.0):
|
||||
- RNSVG (15.14.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2151,9 +2151,9 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNSVG/common (= 15.15.0)
|
||||
- RNSVG/common (= 15.14.0)
|
||||
- Yoga
|
||||
- RNSVG/common (15.15.0):
|
||||
- RNSVG/common (15.14.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2635,7 +2635,7 @@ SPEC CHECKSUMS:
|
||||
RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73
|
||||
RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8
|
||||
RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0
|
||||
RNSVG: 39476f26bbbe72ffe6194c6fc8f6acd588087957
|
||||
RNSVG: e1cf5a9a5aa12c69f2ec47031defbd87ae7fb697
|
||||
segment-analytics-react-native: a0c29c75ede1989118b50cac96b9495ea5c91a1d
|
||||
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
|
||||
@@ -34,9 +34,8 @@ const config = {
|
||||
],
|
||||
|
||||
transformer: {
|
||||
babelTransformerPath: require.resolve(
|
||||
'react-native-svg-transformer/react-native',
|
||||
),
|
||||
babelTransformerPath:
|
||||
require.resolve('react-native-svg-transformer/react-native'),
|
||||
disableImportExportTransform: true,
|
||||
inlineRequires: true,
|
||||
},
|
||||
|
||||
@@ -162,8 +162,8 @@
|
||||
"react-native-safe-area-context": "^5.6.1",
|
||||
"react-native-screens": "4.15.3",
|
||||
"react-native-sqlite-storage": "^6.0.1",
|
||||
"react-native-svg": "^15.14.0",
|
||||
"react-native-svg-web": "^1.0.9",
|
||||
"react-native-svg": "15.14.0",
|
||||
"react-native-svg-web": "1.0.9",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "^0.19.0",
|
||||
"react-native-webview": "^13.16.0",
|
||||
|
||||
@@ -35,8 +35,10 @@ const ModalBackDrop = styled(View, {
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export interface ModalNavigationParams
|
||||
extends Omit<ModalParams, 'onButtonPress' | 'onModalDismiss'> {
|
||||
export interface ModalNavigationParams extends Omit<
|
||||
ModalParams,
|
||||
'onButtonPress' | 'onModalDismiss'
|
||||
> {
|
||||
callbackId: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,9 +132,8 @@ export const getWhiteListedDisclosureAddresses = async (): Promise<
|
||||
export const hasUserAnIdentityDocumentRegistered =
|
||||
async (): Promise<boolean> => {
|
||||
try {
|
||||
const { loadDocumentCatalogDirectlyFromKeychain } = await import(
|
||||
'@/providers/passportDataProvider'
|
||||
);
|
||||
const { loadDocumentCatalogDirectlyFromKeychain } =
|
||||
await import('@/providers/passportDataProvider');
|
||||
const catalog = await loadDocumentCatalogDirectlyFromKeychain();
|
||||
|
||||
return catalog.documents.some(doc => doc.isRegistered === true);
|
||||
|
||||
@@ -129,12 +129,10 @@ export const usePointEventStore = create<PointEventState>()((set, get) => ({
|
||||
|
||||
loadDisclosureEvents: async () => {
|
||||
try {
|
||||
const { getDisclosurePointEvents } = await import(
|
||||
'@/services/points/getEvents'
|
||||
);
|
||||
const { useProofHistoryStore } = await import(
|
||||
'@/stores/proofHistoryStore'
|
||||
);
|
||||
const { getDisclosurePointEvents } =
|
||||
await import('@/services/points/getEvents');
|
||||
const { useProofHistoryStore } =
|
||||
await import('@/stores/proofHistoryStore');
|
||||
await useProofHistoryStore.getState().initDatabase();
|
||||
const disclosureEvents = await getDisclosurePointEvents();
|
||||
const existingEvents = get().events.filter(e => e.type !== 'disclosure');
|
||||
|
||||
214
docs/coverage/app.json
Normal file
214
docs/coverage/app.json
Normal file
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"generatedAt": "2025-12-25T18:56:55.583Z",
|
||||
"label": "Mobile App",
|
||||
"totals": {
|
||||
"exports": 497,
|
||||
"documented": 75,
|
||||
"undocumented": 422,
|
||||
"coverage": 15.09
|
||||
},
|
||||
"undocumentedTotal": 422,
|
||||
"undocumentedSampled": 50,
|
||||
"undocumented": [
|
||||
{
|
||||
"file": "app/src/assets/animations/loader.ts",
|
||||
"symbol": "loadMiscAnimation"
|
||||
},
|
||||
{
|
||||
"file": "app/src/assets/animations/loader.ts",
|
||||
"symbol": "loadPassportAnimation"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/BackupDocumentationLink.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/Disclosures.tsx",
|
||||
"symbol": "default (local: Disclosures)"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/ErrorBoundary.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/FeedbackModal.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/FeedbackModalScreen.tsx",
|
||||
"symbol": "FeedbackModalScreenParams"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/FeedbackModalScreen.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/homescreen/IdCard.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/homescreen/SvgXmlWrapper.native.tsx",
|
||||
"symbol": "SvgXml"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/homescreen/SvgXmlWrapper.native.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/homescreen/SvgXmlWrapper.web.tsx",
|
||||
"symbol": "SvgXml"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/homescreen/SvgXmlWrapper.web.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/LoadingUI.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/Mnemonic.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/PassportCamera.tsx",
|
||||
"symbol": "PassportCameraProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/PassportCamera.tsx",
|
||||
"symbol": "PassportCamera"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/PassportCamera.web.tsx",
|
||||
"symbol": "PassportCameraProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/PassportCamera.web.tsx",
|
||||
"symbol": "PassportCamera"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/QRCodeScanner.tsx",
|
||||
"symbol": "QRCodeScannerViewProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/QRCodeScanner.tsx",
|
||||
"symbol": "QRCodeScannerView"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/QRCodeScanner.web.tsx",
|
||||
"symbol": "QRCodeScannerViewProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/QRCodeScanner.web.tsx",
|
||||
"symbol": "QRCodeScannerView"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/QRCodeScanner.web.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/RCTFragment.tsx",
|
||||
"symbol": "FragmentProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/RCTFragment.tsx",
|
||||
"symbol": "RCTFragmentViewManagerProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/native/RCTFragment.tsx",
|
||||
"symbol": "RCTFragment"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/AadhaarNavBar.tsx",
|
||||
"symbol": "AadhaarNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/BaseNavBar.tsx",
|
||||
"symbol": "LeftAction"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/BaseNavBar.tsx",
|
||||
"symbol": "RightAction"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/BaseNavBar.tsx",
|
||||
"symbol": "NavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/DefaultNavBar.tsx",
|
||||
"symbol": "DefaultNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/DocumentFlowNavBar.tsx",
|
||||
"symbol": "DocumentFlowNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/HeadlessNavForEuclid.tsx",
|
||||
"symbol": "HeadlessNavForEuclid"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/HomeNavBar.tsx",
|
||||
"symbol": "HomeNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/IdDetailsNavBar.tsx",
|
||||
"symbol": "IdDetailsNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/Points.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/PointsNavBar.tsx",
|
||||
"symbol": "PointsNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/WebViewNavBar.tsx",
|
||||
"symbol": "WebViewNavBarProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/navbar/WebViewNavBar.tsx",
|
||||
"symbol": "WebViewNavBar"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/PointHistoryList.tsx",
|
||||
"symbol": "PointHistoryListProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/PointHistoryList.tsx",
|
||||
"symbol": "PointHistoryList"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/PointHistoryList.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/CopyReferralButton.tsx",
|
||||
"symbol": "CopyReferralButtonProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/CopyReferralButton.tsx",
|
||||
"symbol": "CopyReferralButton"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/ReferralHeader.tsx",
|
||||
"symbol": "ReferralHeaderProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/ReferralHeader.tsx",
|
||||
"symbol": "ReferralHeader"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/ReferralInfo.tsx",
|
||||
"symbol": "ReferralInfoProps"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/ReferralInfo.tsx",
|
||||
"symbol": "ReferralInfo"
|
||||
},
|
||||
{
|
||||
"file": "app/src/components/referral/ShareButton.tsx",
|
||||
"symbol": "ShareButtonProps"
|
||||
}
|
||||
]
|
||||
}
|
||||
214
docs/coverage/sdk.json
Normal file
214
docs/coverage/sdk.json
Normal file
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"generatedAt": "2025-12-25T18:56:56.987Z",
|
||||
"label": "Mobile SDK Alpha",
|
||||
"totals": {
|
||||
"exports": 234,
|
||||
"documented": 77,
|
||||
"undocumented": 157,
|
||||
"coverage": 32.91
|
||||
},
|
||||
"undocumentedTotal": 157,
|
||||
"undocumentedSampled": 50,
|
||||
"undocumented": [
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/adapters/react-native/nfc-scanner.ts",
|
||||
"symbol": "reactNativeScannerAdapter"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/adapters/web/shims.ts",
|
||||
"symbol": "webNFCScannerShim"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.native.ts",
|
||||
"symbol": "addListener"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.native.ts",
|
||||
"symbol": "removeListener"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.ts",
|
||||
"symbol": "EventHandler"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.ts",
|
||||
"symbol": "NativeEventBridge"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.ts",
|
||||
"symbol": "addListener"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/bridge/nativeEvents.ts",
|
||||
"symbol": "removeListener"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/AbstractButton.tsx",
|
||||
"symbol": "ButtonProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/AbstractButton.tsx",
|
||||
"symbol": "default (local: AbstractButton)"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/HeldPrimaryButtonProveScreen.tsx",
|
||||
"symbol": "HeldPrimaryButtonProveScreen"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/pressedStyle.tsx",
|
||||
"symbol": "pressedStyle"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButton.tsx",
|
||||
"symbol": "PrimaryButton"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.shared.ts",
|
||||
"symbol": "HeldPrimaryButtonProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.shared.ts",
|
||||
"symbol": "RGBA"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.shared.ts",
|
||||
"symbol": "ACTION_TIMER"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.shared.ts",
|
||||
"symbol": "COLORS"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.tsx",
|
||||
"symbol": "HeldPrimaryButton"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/PrimaryButtonLongHold.web.tsx",
|
||||
"symbol": "HeldPrimaryButton"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/buttons/SecondaryButton.tsx",
|
||||
"symbol": "SecondaryButton"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/ButtonsContainer.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/flag/RoundFlag.tsx",
|
||||
"symbol": "RoundFlag"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/Button.tsx",
|
||||
"symbol": "Button"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/Text.tsx",
|
||||
"symbol": "Text"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/View.tsx",
|
||||
"symbol": "ViewProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/View.tsx",
|
||||
"symbol": "View"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/XStack.tsx",
|
||||
"symbol": "XStack"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/layout/YStack.tsx",
|
||||
"symbol": "YStack"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/MRZScannerView.tsx",
|
||||
"symbol": "MRZScannerViewProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/MRZScannerView.tsx",
|
||||
"symbol": "MRZScannerView"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/MRZScannerView.tsx",
|
||||
"symbol": "SelfMRZScannerModule"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/RCTFragment.tsx",
|
||||
"symbol": "FragmentProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/RCTFragment.tsx",
|
||||
"symbol": "RCTFragmentViewManagerProps"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/RCTFragment.tsx",
|
||||
"symbol": "RCTFragment"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/screens/NFCScannerScreen.tsx",
|
||||
"symbol": "NFCScannerScreen"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx",
|
||||
"symbol": "PassportCameraScreen"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/screens/QRCodeScreen.tsx",
|
||||
"symbol": "QRCodeScreen"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/TextsContainer.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/Additional.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/BodyText.tsx",
|
||||
"symbol": "BodyText"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/Caption.tsx",
|
||||
"symbol": "Caption"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/Caution.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/Description.tsx",
|
||||
"symbol": "default export"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/DescriptionTitle.tsx",
|
||||
"symbol": "DescriptionTitle"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/styles.ts",
|
||||
"symbol": "typography"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/SubHeader.tsx",
|
||||
"symbol": "SubHeader"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/components/typography/Title.tsx",
|
||||
"symbol": "Title"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/config/defaults.ts",
|
||||
"symbol": "defaultConfig"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/config/merge.ts",
|
||||
"symbol": "mergeConfig"
|
||||
},
|
||||
{
|
||||
"file": "packages/mobile-sdk-alpha/src/constants/analytics.ts",
|
||||
"symbol": "AadhaarEvents"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
# Self App Development Patterns
|
||||
|
||||
## Docstring coverage workflow
|
||||
|
||||
- Run `yarn docstrings` to check documentation coverage for both the mobile app and SDK. This generates `docs/coverage/app.json` and `docs/coverage/sdk.json` so you can diff coverage changes in version control.
|
||||
- Run `yarn docstrings:app` to check only the mobile app exports.
|
||||
- Run `yarn docstrings:sdk` to focus on `@selfxyz/mobile-sdk-alpha` only.
|
||||
- Add `--details` to any command when you want a full per-file JSON breakdown for ad-hoc analysis—the default snapshots include only top-level totals and a small sample of undocumented exports to keep the tracked files compact.
|
||||
|
||||
Run the docstring reports locally before committing to track coverage changes. The reports are advisory—use them to identify documentation gaps but they won't block builds.
|
||||
|
||||
## React Native Architecture
|
||||
|
||||
### Navigation System
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
"build:demo": "yarn workspace mobile-sdk-demo build",
|
||||
"build:mobile-sdk": "yarn workspace @selfxyz/mobile-sdk-alpha build",
|
||||
"check:versions": "node scripts/check-package-versions.mjs",
|
||||
"docstrings": "yarn docstrings:app && yarn docstrings:sdk",
|
||||
"docstrings:app": "yarn tsx scripts/docstring-report.ts \"app/src/**/*.{ts,tsx}\" --label \"Mobile App\" --write-report docs/coverage/app.json",
|
||||
"docstrings:sdk": "yarn tsx scripts/docstring-report.ts \"packages/mobile-sdk-alpha/src/**/*.{ts,tsx}\" --label \"Mobile SDK Alpha\" --write-report docs/coverage/sdk.json",
|
||||
"demo:mobile": "yarn build:mobile-sdk && yarn build:demo && yarn workspace mobile-sdk-demo start",
|
||||
"format": "SKIP_BUILD_DEPS=1 yarn format:root && yarn format:github && SKIP_BUILD_DEPS=1 yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run format",
|
||||
"format:github": "yarn prettier --parser yaml --write .github/**/*.yml --single-quote false",
|
||||
@@ -65,9 +68,10 @@
|
||||
"knip": "^5.63.1",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"engines": {
|
||||
"node": ">=22 <23"
|
||||
}
|
||||
|
||||
14
packages/mobile-sdk-alpha/docs/DOCSTRING_STYLE_GUIDE.md
Normal file
14
packages/mobile-sdk-alpha/docs/DOCSTRING_STYLE_GUIDE.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Mobile SDK docstring style guide
|
||||
|
||||
All exported APIs from `packages/mobile-sdk-alpha/src` must carry TSDoc-compliant comments so integrators can rely on generated documentation and in-editor hints.
|
||||
|
||||
## Authoring guidelines
|
||||
|
||||
- Start each docstring with a one-line summary that describes the intent of the API in the imperative mood.
|
||||
- Describe complex parameter shapes with `@param` tags and consider linking to shared types with `{@link ...}` when the name alone is ambiguous.
|
||||
- Capture platform nuances (for example, “Android only”) and error semantics in the main description or an `@remarks` block.
|
||||
- Prefer examples that demonstrate the supported developer experience (React Native, Expo, etc.) and keep them short enough to scan quickly.
|
||||
|
||||
## Coverage expectations
|
||||
|
||||
`yarn docstrings:sdk` (or `yarn docstrings` for both app and SDK) surfaces the current coverage numbers in `docs/coverage/*.json`. The reports can be committed to track progress over time. Coverage thresholds are advisory—use the reports to plan follow-up work even when you need to land code without full documentation.
|
||||
@@ -47,10 +47,7 @@ interface PressableViewProps {
|
||||
}
|
||||
|
||||
export interface ViewProps
|
||||
extends Omit<RNViewProps, 'hitSlop'>,
|
||||
SpacingProps,
|
||||
Omit<ViewStyle, keyof SpacingProps>,
|
||||
PressableViewProps {}
|
||||
extends Omit<RNViewProps, 'hitSlop'>, SpacingProps, Omit<ViewStyle, keyof SpacingProps>, PressableViewProps {}
|
||||
|
||||
const sizeTokens: Record<string, number> = {
|
||||
$0: 0,
|
||||
|
||||
804
scripts/docstring-report.ts
Normal file
804
scripts/docstring-report.ts
Normal file
@@ -0,0 +1,804 @@
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import fs from 'node:fs/promises';
|
||||
import { glob } from 'node:fs/promises';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
interface CliOptions {
|
||||
patterns: string[];
|
||||
writeReport?: string;
|
||||
label?: string;
|
||||
includeDetails: boolean;
|
||||
}
|
||||
|
||||
interface ExportEntry {
|
||||
localName: string;
|
||||
kinds: Set<string>;
|
||||
exportedAs: Set<string>;
|
||||
documented: boolean;
|
||||
exported: boolean;
|
||||
}
|
||||
|
||||
interface FileExportSummary {
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
totalExports: number;
|
||||
documentedExports: number;
|
||||
coverage: number;
|
||||
missing: string[];
|
||||
}
|
||||
|
||||
interface JsonReport {
|
||||
generatedAt: string;
|
||||
label?: string;
|
||||
totals: {
|
||||
exports: number;
|
||||
documented: number;
|
||||
undocumented: number;
|
||||
coverage: number;
|
||||
};
|
||||
undocumentedTotal: number;
|
||||
undocumentedSampled: number;
|
||||
undocumented: UndocumentedEntry[];
|
||||
files?: JsonReportFile[];
|
||||
}
|
||||
|
||||
interface JsonReportFile {
|
||||
file: string;
|
||||
exports: number;
|
||||
documented: number;
|
||||
undocumented: number;
|
||||
coverage: number;
|
||||
missing: string[];
|
||||
}
|
||||
|
||||
interface UndocumentedEntry {
|
||||
file: string;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
const DEFAULT_PATTERNS = [
|
||||
'app/src/**/*.{ts,tsx}',
|
||||
'packages/mobile-sdk-alpha/src/**/*.{ts,tsx}',
|
||||
];
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const root = process.cwd();
|
||||
const files = await resolveFiles(options.patterns, root);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No source files matched the provided patterns.');
|
||||
if (options.writeReport) {
|
||||
await writeJsonReport(options.writeReport, {
|
||||
generatedAt: new Date().toISOString(),
|
||||
label: options.label,
|
||||
totals: { exports: 0, documented: 0, undocumented: 0, coverage: 100 },
|
||||
undocumentedTotal: 0,
|
||||
undocumentedSampled: 0,
|
||||
undocumented: [],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const summaries: FileExportSummary[] = [];
|
||||
const failedFiles: Array<{ path: string; error: string }> = [];
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const summary = await analyzeFile(filePath, root);
|
||||
if (summary.totalExports > 0) {
|
||||
summaries.push(summary);
|
||||
}
|
||||
} catch (error) {
|
||||
const relativePath = path.relative(root, filePath);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
failedFiles.push({ path: relativePath, error: errorMessage });
|
||||
console.error(`Failed to analyze ${relativePath}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (summaries.length === 0) {
|
||||
console.log('No exported declarations were found in the selected files.');
|
||||
if (options.writeReport) {
|
||||
await writeJsonReport(options.writeReport, {
|
||||
generatedAt: new Date().toISOString(),
|
||||
label: options.label,
|
||||
totals: { exports: 0, documented: 0, undocumented: 0, coverage: 100 },
|
||||
undocumentedTotal: 0,
|
||||
undocumentedSampled: 0,
|
||||
undocumented: [],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
summaries.sort((a, b) => {
|
||||
if (a.coverage === b.coverage) {
|
||||
return a.relativePath.localeCompare(b.relativePath);
|
||||
}
|
||||
return a.coverage - b.coverage;
|
||||
});
|
||||
|
||||
const totalExports = summaries.reduce(
|
||||
(sum, file) => sum + file.totalExports,
|
||||
0,
|
||||
);
|
||||
const documentedExports = summaries.reduce(
|
||||
(sum, file) => sum + file.documentedExports,
|
||||
0,
|
||||
);
|
||||
const overallCoverage =
|
||||
totalExports === 0 ? 1 : documentedExports / totalExports;
|
||||
|
||||
printTable(summaries, options.label);
|
||||
printSummary(totalExports, documentedExports, overallCoverage);
|
||||
printUndocumentedHighlights(summaries);
|
||||
|
||||
if (failedFiles.length > 0) {
|
||||
console.log();
|
||||
console.log(`Failed to analyze ${failedFiles.length} file(s):`);
|
||||
for (const failure of failedFiles) {
|
||||
console.log(` ${failure.path}: ${failure.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.writeReport) {
|
||||
const missingEntries = summaries.flatMap(file =>
|
||||
file.missing.map<UndocumentedEntry>(symbol => ({
|
||||
file: file.relativePath,
|
||||
symbol,
|
||||
})),
|
||||
);
|
||||
const maxUndocumentedEntries = options.includeDetails
|
||||
? missingEntries.length
|
||||
: Math.min(50, missingEntries.length);
|
||||
const files = options.includeDetails
|
||||
? summaries
|
||||
.filter(file => file.missing.length > 0)
|
||||
.map<JsonReportFile>(file => ({
|
||||
file: file.relativePath,
|
||||
exports: file.totalExports,
|
||||
documented: file.documentedExports,
|
||||
undocumented: file.totalExports - file.documentedExports,
|
||||
coverage: Number((file.coverage * 100).toFixed(2)),
|
||||
missing: file.missing,
|
||||
}))
|
||||
: undefined;
|
||||
const report: JsonReport = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
label: options.label,
|
||||
totals: {
|
||||
exports: totalExports,
|
||||
documented: documentedExports,
|
||||
undocumented: totalExports - documentedExports,
|
||||
coverage: Number((overallCoverage * 100).toFixed(2)),
|
||||
},
|
||||
undocumentedTotal: missingEntries.length,
|
||||
undocumentedSampled: maxUndocumentedEntries,
|
||||
undocumented: missingEntries.slice(0, maxUndocumentedEntries),
|
||||
...(files ? { files } : {}),
|
||||
};
|
||||
|
||||
await writeJsonReport(options.writeReport, report);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate docstring report.');
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(args: string[]): CliOptions {
|
||||
const patterns: string[] = [];
|
||||
let writeReport: string | undefined;
|
||||
let label: string | undefined;
|
||||
let includeDetails = false;
|
||||
|
||||
const expectValue = (flag: string, value: string | undefined): string => {
|
||||
if (!value) {
|
||||
throw new Error(`Missing value for ${flag}`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (arg === '--write-report' || arg.startsWith('--write-report=')) {
|
||||
if (arg.includes('=')) {
|
||||
writeReport = arg.split('=')[1] ?? '';
|
||||
if (!writeReport) {
|
||||
throw new Error('Missing value for --write-report');
|
||||
}
|
||||
} else {
|
||||
index += 1;
|
||||
writeReport = expectValue('--write-report', args[index]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--label' || arg.startsWith('--label=')) {
|
||||
if (arg.includes('=')) {
|
||||
label = arg.split('=')[1] ?? '';
|
||||
} else {
|
||||
index += 1;
|
||||
label = expectValue('--label', args[index]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--')) {
|
||||
if (arg === '--details') {
|
||||
includeDetails = true;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown option: ${arg}`);
|
||||
}
|
||||
|
||||
patterns.push(arg);
|
||||
}
|
||||
|
||||
if (patterns.length === 0) {
|
||||
patterns.push(...DEFAULT_PATTERNS);
|
||||
}
|
||||
|
||||
return { patterns, writeReport, label, includeDetails };
|
||||
}
|
||||
|
||||
function printUsage(): void {
|
||||
const usage = `Usage: docstring-report [pattern ...] [--write-report <path>] [--label <name>] [--details]
|
||||
|
||||
Examples:
|
||||
yarn tsx scripts/docstring-report.ts
|
||||
yarn tsx scripts/docstring-report.ts \"app/src/**/*.{ts,tsx}\"
|
||||
yarn tsx scripts/docstring-report.ts \"app/src/**/*.{ts,tsx}\" --label \"Mobile App\" --write-report docs/coverage/app.json --details`;
|
||||
console.log(usage);
|
||||
}
|
||||
|
||||
async function resolveFiles(
|
||||
patterns: string[],
|
||||
root: string,
|
||||
): Promise<string[]> {
|
||||
const files = new Set<string>();
|
||||
|
||||
for (const pattern of patterns) {
|
||||
for await (const match of glob(pattern, {
|
||||
cwd: root,
|
||||
// Exclude dotfiles and dot-directories
|
||||
exclude: (name: string) => path.basename(name).startsWith('.'),
|
||||
})) {
|
||||
const resolved = path.resolve(root, String(match));
|
||||
|
||||
// Skip directories (glob may return them despite file extension patterns)
|
||||
try {
|
||||
const stat = await fs.stat(resolved);
|
||||
if (stat.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist or can't be accessed, skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldIncludeFile(resolved, root)) {
|
||||
files.add(resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(files).sort();
|
||||
}
|
||||
|
||||
function shouldIncludeFile(filePath: string, root: string): boolean {
|
||||
const relative = path.relative(root, filePath).replace(/\\/g, '/');
|
||||
|
||||
if (relative.endsWith('.d.ts') || relative.endsWith('.d.tsx')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/\.test\.[tj]sx?$/.test(relative) || /\.spec\.[tj]sx?$/.test(relative)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/\.stories\.[tj]sx?$/.test(relative)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (relative.includes('/__tests__/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function analyzeFile(
|
||||
filePath: string,
|
||||
root: string,
|
||||
): Promise<FileExportSummary> {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const scriptKind = filePath.endsWith('.tsx')
|
||||
? ts.ScriptKind.TSX
|
||||
: ts.ScriptKind.TS;
|
||||
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
scriptKind,
|
||||
);
|
||||
|
||||
const entries = new Map<string, ExportEntry>();
|
||||
const exportSpecifiers: Array<{ localName: string; exportedAs: string }> = [];
|
||||
const exportDefaultStatements: ts.ExportAssignment[] = [];
|
||||
const exportedDeclarations: Array<{
|
||||
statement: ts.Statement;
|
||||
hasDefault: boolean;
|
||||
}> = [];
|
||||
|
||||
// First pass: Collect all declarations with their documentation status
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (ts.isExportDeclaration(statement)) {
|
||||
// Collect export specifiers for second pass
|
||||
if (
|
||||
!statement.moduleSpecifier &&
|
||||
statement.exportClause &&
|
||||
ts.isNamedExports(statement.exportClause)
|
||||
) {
|
||||
for (const element of statement.exportClause.elements) {
|
||||
const localName = element.propertyName
|
||||
? element.propertyName.text
|
||||
: element.name.text;
|
||||
const exportedAs = element.name.text;
|
||||
exportSpecifiers.push({ localName, exportedAs });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ts.isExportAssignment(statement)) {
|
||||
if (!statement.isExportEquals) {
|
||||
exportDefaultStatements.push(statement);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ts.isVariableStatement(statement)) {
|
||||
const exported = hasExportModifier(statement.modifiers);
|
||||
const statementDoc = hasDocComment(statement, sourceFile);
|
||||
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
// Extract all binding identifiers (handles destructuring)
|
||||
const identifiers = getBindingIdentifiers(declaration);
|
||||
if (identifiers.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const declarationDoc = hasDocComment(declaration, sourceFile);
|
||||
|
||||
for (const name of identifiers) {
|
||||
const entry = ensureEntry(entries, name);
|
||||
entry.kinds.add('variable');
|
||||
entry.documented ||= statementDoc || declarationDoc;
|
||||
}
|
||||
|
||||
if (exported) {
|
||||
exportedDeclarations.push({ statement, hasDefault: false });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isFunctionDeclaration(statement) ||
|
||||
ts.isClassDeclaration(statement) ||
|
||||
ts.isInterfaceDeclaration(statement) ||
|
||||
ts.isTypeAliasDeclaration(statement) ||
|
||||
ts.isEnumDeclaration(statement) ||
|
||||
ts.isModuleDeclaration(statement)
|
||||
) {
|
||||
const name = getDeclarationName(statement, sourceFile);
|
||||
const hasExport = hasExportModifier(statement.modifiers);
|
||||
const hasDefault = hasDefaultModifier(statement.modifiers);
|
||||
|
||||
// For anonymous default exports (e.g., export default function() {}),
|
||||
// use "default" as the name so they're tracked in coverage
|
||||
const effectiveName = !name && hasExport && hasDefault ? 'default' : name;
|
||||
|
||||
if (!effectiveName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = ensureEntry(entries, effectiveName);
|
||||
entry.kinds.add(getKindLabel(statement));
|
||||
entry.documented ||= hasDocComment(statement, sourceFile);
|
||||
|
||||
if (hasExport) {
|
||||
exportedDeclarations.push({ statement, hasDefault });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: Process all exports now that all declarations are collected
|
||||
// Process inline exported declarations
|
||||
for (const { statement, hasDefault } of exportedDeclarations) {
|
||||
if (ts.isVariableStatement(statement)) {
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
// Extract all binding identifiers (handles destructuring)
|
||||
const identifiers = getBindingIdentifiers(declaration);
|
||||
|
||||
for (const name of identifiers) {
|
||||
const entry = entries.get(name);
|
||||
if (entry) {
|
||||
entry.exported = true;
|
||||
entry.exportedAs.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const name = getDeclarationName(statement, sourceFile);
|
||||
|
||||
// For anonymous default exports, use "default" as the name
|
||||
const effectiveName = !name && hasDefault ? 'default' : name;
|
||||
|
||||
if (!effectiveName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = entries.get(effectiveName);
|
||||
if (entry) {
|
||||
entry.exported = true;
|
||||
// For inline default exports (export default function foo), add "default" not the name
|
||||
const exportName = hasDefault ? 'default' : effectiveName;
|
||||
entry.exportedAs.add(exportName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process export specifiers (export { Foo, Bar })
|
||||
for (const specifier of exportSpecifiers) {
|
||||
const entry = entries.get(specifier.localName);
|
||||
if (entry) {
|
||||
entry.exported = true;
|
||||
entry.exportedAs.add(specifier.exportedAs);
|
||||
}
|
||||
}
|
||||
|
||||
// Process export default statements (export default Foo)
|
||||
for (const statement of exportDefaultStatements) {
|
||||
const entry = ensureEntry(entries, 'default');
|
||||
entry.exported = true;
|
||||
entry.kinds.add('default');
|
||||
entry.exportedAs.add('default');
|
||||
|
||||
// Check if the export statement itself is documented
|
||||
entry.documented ||= hasDocComment(statement, sourceFile);
|
||||
|
||||
// If exporting an identifier (export default Foo), inherit documentation from the referenced declaration
|
||||
if (ts.isIdentifier(statement.expression)) {
|
||||
const referencedName = statement.expression.text;
|
||||
const referencedEntry = entries.get(referencedName);
|
||||
if (referencedEntry?.documented) {
|
||||
entry.documented = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const relativePath = path.relative(root, filePath).replace(/\\/g, '/');
|
||||
const exportedEntries = Array.from(entries.values()).filter(
|
||||
entry => entry.exported,
|
||||
);
|
||||
const documentedEntries = exportedEntries.filter(entry => entry.documented);
|
||||
|
||||
const missing = exportedEntries
|
||||
.filter(entry => !entry.documented)
|
||||
.map(entry => formatMissingName(entry));
|
||||
|
||||
return {
|
||||
filePath,
|
||||
relativePath,
|
||||
totalExports: exportedEntries.length,
|
||||
documentedExports: documentedEntries.length,
|
||||
coverage:
|
||||
exportedEntries.length === 0
|
||||
? 1
|
||||
: documentedEntries.length / exportedEntries.length,
|
||||
missing,
|
||||
};
|
||||
}
|
||||
|
||||
function ensureEntry(map: Map<string, ExportEntry>, key: string): ExportEntry {
|
||||
const existing = map.get(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const entry: ExportEntry = {
|
||||
localName: key,
|
||||
kinds: new Set<string>(),
|
||||
exportedAs: new Set<string>(),
|
||||
documented: false,
|
||||
exported: false,
|
||||
};
|
||||
|
||||
map.set(key, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
function hasExportModifier(
|
||||
modifiers: ts.NodeArray<ts.Modifier> | undefined,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
modifiers?.some(
|
||||
modifier =>
|
||||
modifier.kind === ts.SyntaxKind.ExportKeyword ||
|
||||
modifier.kind === ts.SyntaxKind.DefaultKeyword,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function hasDefaultModifier(
|
||||
modifiers: ts.NodeArray<ts.Modifier> | undefined,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword),
|
||||
);
|
||||
}
|
||||
|
||||
function getDeclarationName(
|
||||
node: ts.Node,
|
||||
sourceFile: ts.SourceFile,
|
||||
): string | undefined {
|
||||
if ('name' in node && node.name) {
|
||||
const nameNode = (node as ts.Node & { name?: ts.Node }).name as
|
||||
| ts.Node
|
||||
| undefined;
|
||||
if (!nameNode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isIdentifier(nameNode) ||
|
||||
ts.isStringLiteralLike(nameNode) ||
|
||||
ts.isNumericLiteral(nameNode)
|
||||
) {
|
||||
return nameNode.text;
|
||||
}
|
||||
|
||||
return nameNode.getText(sourceFile).trim();
|
||||
}
|
||||
|
||||
if (ts.isModuleDeclaration(node)) {
|
||||
return node.name.text;
|
||||
}
|
||||
|
||||
if (ts.isExportAssignment(node)) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all binding identifiers from a declaration.
|
||||
* Handles destructuring patterns like { a, b } and [x, y].
|
||||
*/
|
||||
function getBindingIdentifiers(declaration: ts.VariableDeclaration): string[] {
|
||||
const identifiers: string[] = [];
|
||||
|
||||
function collectIdentifiers(name: ts.BindingName): void {
|
||||
if (ts.isIdentifier(name)) {
|
||||
identifiers.push(name.text);
|
||||
} else if (ts.isObjectBindingPattern(name)) {
|
||||
for (const element of name.elements) {
|
||||
collectIdentifiers(element.name);
|
||||
}
|
||||
} else if (ts.isArrayBindingPattern(name)) {
|
||||
for (const element of name.elements) {
|
||||
if (ts.isBindingElement(element)) {
|
||||
collectIdentifiers(element.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectIdentifiers(declaration.name);
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
function getKindLabel(node: ts.Node): string {
|
||||
if (ts.isFunctionDeclaration(node)) {
|
||||
return 'function';
|
||||
}
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
return 'class';
|
||||
}
|
||||
if (ts.isInterfaceDeclaration(node)) {
|
||||
return 'interface';
|
||||
}
|
||||
if (ts.isTypeAliasDeclaration(node)) {
|
||||
return 'type';
|
||||
}
|
||||
if (ts.isEnumDeclaration(node)) {
|
||||
return 'enum';
|
||||
}
|
||||
if (ts.isModuleDeclaration(node)) {
|
||||
return 'namespace';
|
||||
}
|
||||
return 'declaration';
|
||||
}
|
||||
|
||||
function hasDocComment(node: ts.Node, sourceFile: ts.SourceFile): boolean {
|
||||
const jsDocNodes = (node as ts.Node & { jsDoc?: readonly ts.JSDoc[] }).jsDoc;
|
||||
if (jsDocNodes && jsDocNodes.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const jsDocRanges = ts.getJSDocCommentRanges(node, sourceFile.text);
|
||||
if (jsDocRanges && jsDocRanges.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const leadingRanges = ts.getLeadingCommentRanges(
|
||||
sourceFile.text,
|
||||
node.getFullStart(),
|
||||
);
|
||||
if (leadingRanges) {
|
||||
return leadingRanges.some(range =>
|
||||
sourceFile.text.slice(range.pos, range.end).startsWith('/**'),
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function formatPercent(value: number): string {
|
||||
return `${(value * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
function printTable(summaries: FileExportSummary[], label?: string): void {
|
||||
const title = label ? `Docstring coverage (${label})` : 'Docstring coverage';
|
||||
console.log(title);
|
||||
console.log('='.repeat(title.length));
|
||||
|
||||
const headers = ['File', 'Exports', 'With Docs', 'Coverage', 'Missing'];
|
||||
const rows = summaries.map(summary => [
|
||||
summary.relativePath,
|
||||
summary.totalExports.toString(),
|
||||
summary.documentedExports.toString(),
|
||||
formatPercent(summary.coverage),
|
||||
summary.missing.join(', '),
|
||||
]);
|
||||
|
||||
const widths = headers.map((header, columnIndex) => {
|
||||
const columnValues = rows.map(row => row[columnIndex]);
|
||||
const maxContentLength = columnValues.reduce(
|
||||
(max, value) => Math.max(max, value.length),
|
||||
header.length,
|
||||
);
|
||||
const maxWidth =
|
||||
columnIndex === 0
|
||||
? Math.min(70, Math.max(20, maxContentLength))
|
||||
: maxContentLength;
|
||||
return maxWidth;
|
||||
});
|
||||
|
||||
const formatRow = (values: string[]): string =>
|
||||
values
|
||||
.map((value, index) => {
|
||||
const width = widths[index];
|
||||
const trimmed =
|
||||
index === 0 && value.length > width
|
||||
? `…${value.slice(value.length - width + 1)}`
|
||||
: value;
|
||||
return trimmed.padEnd(width, ' ');
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
console.log(formatRow(headers));
|
||||
console.log(
|
||||
formatRow(
|
||||
widths.map(width => '-'.repeat(Math.max(3, Math.min(width, 80)))),
|
||||
),
|
||||
);
|
||||
rows.forEach(row => console.log(formatRow(row)));
|
||||
}
|
||||
|
||||
function printSummary(
|
||||
total: number,
|
||||
documented: number,
|
||||
coverage: number,
|
||||
): void {
|
||||
console.log();
|
||||
if (total === 0) {
|
||||
console.log('Overall coverage: 100.00% (0/0 exported declarations)');
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`Overall coverage: ${formatPercent(coverage)} (${documented}/${total} exported declarations documented)`,
|
||||
);
|
||||
}
|
||||
|
||||
function printUndocumentedHighlights(summaries: FileExportSummary[]): void {
|
||||
const missingEntries: Array<{ file: string; names: string[] }> = [];
|
||||
for (const summary of summaries) {
|
||||
if (summary.missing.length > 0) {
|
||||
missingEntries.push({
|
||||
file: summary.relativePath,
|
||||
names: summary.missing,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (missingEntries.length === 0) {
|
||||
console.log('All exported declarations include TSDoc comments.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log('Undocumented exports:');
|
||||
for (const entry of missingEntries) {
|
||||
console.log(` ${entry.file}`);
|
||||
for (const name of entry.names) {
|
||||
console.log(` - ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatMissingName(entry: ExportEntry): string {
|
||||
const exportedNames = Array.from(entry.exportedAs);
|
||||
if (exportedNames.length === 0) {
|
||||
return entry.localName;
|
||||
}
|
||||
|
||||
const aliasList = exportedNames.filter(
|
||||
name => name !== entry.localName && name !== 'default',
|
||||
);
|
||||
if (exportedNames.includes('default')) {
|
||||
if (aliasList.length > 0) {
|
||||
return `default (local: ${entry.localName}, aliases: ${aliasList.join(', ')})`;
|
||||
}
|
||||
if (entry.localName !== 'default') {
|
||||
return `default (local: ${entry.localName})`;
|
||||
}
|
||||
return 'default export';
|
||||
}
|
||||
|
||||
if (aliasList.length > 0) {
|
||||
return `${aliasList.join(', ')} (local: ${entry.localName})`;
|
||||
}
|
||||
|
||||
return entry.localName;
|
||||
}
|
||||
|
||||
async function writeJsonReport(
|
||||
targetPath: string,
|
||||
report: JsonReport,
|
||||
): Promise<void> {
|
||||
const resolvedPath = path.resolve(process.cwd(), targetPath);
|
||||
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
resolvedPath,
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
console.log(
|
||||
`\nSaved coverage snapshot to ${path.relative(process.cwd(), resolvedPath)}`,
|
||||
);
|
||||
}
|
||||
|
||||
void main();
|
||||
Reference in New Issue
Block a user