driftdetect-detectors 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/dist/accessibility/alt-text.d.ts +63 -0
- package/dist/accessibility/alt-text.d.ts.map +1 -0
- package/dist/accessibility/alt-text.js +100 -0
- package/dist/accessibility/alt-text.js.map +1 -0
- package/dist/accessibility/aria-roles.d.ts +65 -0
- package/dist/accessibility/aria-roles.d.ts.map +1 -0
- package/dist/accessibility/aria-roles.js +87 -0
- package/dist/accessibility/aria-roles.js.map +1 -0
- package/dist/accessibility/focus-management.d.ts +62 -0
- package/dist/accessibility/focus-management.d.ts.map +1 -0
- package/dist/accessibility/focus-management.js +88 -0
- package/dist/accessibility/focus-management.js.map +1 -0
- package/dist/accessibility/heading-hierarchy.d.ts +66 -0
- package/dist/accessibility/heading-hierarchy.d.ts.map +1 -0
- package/dist/accessibility/heading-hierarchy.js +94 -0
- package/dist/accessibility/heading-hierarchy.js.map +1 -0
- package/dist/accessibility/index.d.ts +25 -0
- package/dist/accessibility/index.d.ts.map +1 -0
- package/dist/accessibility/index.js +21 -0
- package/dist/accessibility/index.js.map +1 -0
- package/dist/accessibility/keyboard-nav.d.ts +63 -0
- package/dist/accessibility/keyboard-nav.d.ts.map +1 -0
- package/dist/accessibility/keyboard-nav.js +86 -0
- package/dist/accessibility/keyboard-nav.js.map +1 -0
- package/dist/accessibility/semantic-html.d.ts +76 -0
- package/dist/accessibility/semantic-html.d.ts.map +1 -0
- package/dist/accessibility/semantic-html.js +204 -0
- package/dist/accessibility/semantic-html.js.map +1 -0
- package/dist/api/client-patterns.d.ts +121 -0
- package/dist/api/client-patterns.d.ts.map +1 -0
- package/dist/api/client-patterns.js +478 -0
- package/dist/api/client-patterns.js.map +1 -0
- package/dist/api/error-format.d.ts +140 -0
- package/dist/api/error-format.d.ts.map +1 -0
- package/dist/api/error-format.js +614 -0
- package/dist/api/error-format.js.map +1 -0
- package/dist/api/http-methods.d.ts +255 -0
- package/dist/api/http-methods.d.ts.map +1 -0
- package/dist/api/http-methods.js +890 -0
- package/dist/api/http-methods.js.map +1 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +37 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/pagination.d.ts +133 -0
- package/dist/api/pagination.d.ts.map +1 -0
- package/dist/api/pagination.js +521 -0
- package/dist/api/pagination.js.map +1 -0
- package/dist/api/response-envelope.d.ts +261 -0
- package/dist/api/response-envelope.d.ts.map +1 -0
- package/dist/api/response-envelope.js +1050 -0
- package/dist/api/response-envelope.js.map +1 -0
- package/dist/api/retry-patterns.d.ts +117 -0
- package/dist/api/retry-patterns.d.ts.map +1 -0
- package/dist/api/retry-patterns.js +480 -0
- package/dist/api/retry-patterns.js.map +1 -0
- package/dist/api/route-structure.d.ts +128 -0
- package/dist/api/route-structure.d.ts.map +1 -0
- package/dist/api/route-structure.js +738 -0
- package/dist/api/route-structure.js.map +1 -0
- package/dist/auth/audit-logging.d.ts +80 -0
- package/dist/auth/audit-logging.d.ts.map +1 -0
- package/dist/auth/audit-logging.js +370 -0
- package/dist/auth/audit-logging.js.map +1 -0
- package/dist/auth/index.d.ts +33 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +49 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware-usage.d.ts +65 -0
- package/dist/auth/middleware-usage.d.ts.map +1 -0
- package/dist/auth/middleware-usage.js +192 -0
- package/dist/auth/middleware-usage.js.map +1 -0
- package/dist/auth/permission-checks.d.ts +60 -0
- package/dist/auth/permission-checks.d.ts.map +1 -0
- package/dist/auth/permission-checks.js +159 -0
- package/dist/auth/permission-checks.js.map +1 -0
- package/dist/auth/rbac-patterns.d.ts +68 -0
- package/dist/auth/rbac-patterns.d.ts.map +1 -0
- package/dist/auth/rbac-patterns.js +143 -0
- package/dist/auth/rbac-patterns.js.map +1 -0
- package/dist/auth/resource-ownership.d.ts +77 -0
- package/dist/auth/resource-ownership.d.ts.map +1 -0
- package/dist/auth/resource-ownership.js +324 -0
- package/dist/auth/resource-ownership.js.map +1 -0
- package/dist/auth/token-handling.d.ts +64 -0
- package/dist/auth/token-handling.d.ts.map +1 -0
- package/dist/auth/token-handling.js +151 -0
- package/dist/auth/token-handling.js.map +1 -0
- package/dist/base/ast-detector.d.ts +421 -0
- package/dist/base/ast-detector.d.ts.map +1 -0
- package/dist/base/ast-detector.js +699 -0
- package/dist/base/ast-detector.js.map +1 -0
- package/dist/base/base-detector.d.ts +366 -0
- package/dist/base/base-detector.d.ts.map +1 -0
- package/dist/base/base-detector.js +170 -0
- package/dist/base/base-detector.js.map +1 -0
- package/dist/base/index.d.ts +12 -0
- package/dist/base/index.d.ts.map +1 -0
- package/dist/base/index.js +17 -0
- package/dist/base/index.js.map +1 -0
- package/dist/base/regex-detector.d.ts +421 -0
- package/dist/base/regex-detector.d.ts.map +1 -0
- package/dist/base/regex-detector.js +537 -0
- package/dist/base/regex-detector.js.map +1 -0
- package/dist/base/structural-detector.d.ts +424 -0
- package/dist/base/structural-detector.d.ts.map +1 -0
- package/dist/base/structural-detector.js +731 -0
- package/dist/base/structural-detector.js.map +1 -0
- package/dist/base/types.d.ts +53 -0
- package/dist/base/types.d.ts.map +1 -0
- package/dist/base/types.js +5 -0
- package/dist/base/types.js.map +1 -0
- package/dist/components/component-structure.d.ts +163 -0
- package/dist/components/component-structure.d.ts.map +1 -0
- package/dist/components/component-structure.js +500 -0
- package/dist/components/component-structure.js.map +1 -0
- package/dist/components/composition.d.ts +287 -0
- package/dist/components/composition.d.ts.map +1 -0
- package/dist/components/composition.js +1123 -0
- package/dist/components/composition.js.map +1 -0
- package/dist/components/duplicate-detection.d.ts +251 -0
- package/dist/components/duplicate-detection.d.ts.map +1 -0
- package/dist/components/duplicate-detection.js +804 -0
- package/dist/components/duplicate-detection.js.map +1 -0
- package/dist/components/index.d.ts +16 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +51 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/near-duplicate.d.ts +402 -0
- package/dist/components/near-duplicate.d.ts.map +1 -0
- package/dist/components/near-duplicate.js +1090 -0
- package/dist/components/near-duplicate.js.map +1 -0
- package/dist/components/props-patterns.d.ts +194 -0
- package/dist/components/props-patterns.d.ts.map +1 -0
- package/dist/components/props-patterns.js +795 -0
- package/dist/components/props-patterns.js.map +1 -0
- package/dist/components/ref-forwarding.d.ts +250 -0
- package/dist/components/ref-forwarding.d.ts.map +1 -0
- package/dist/components/ref-forwarding.js +832 -0
- package/dist/components/ref-forwarding.js.map +1 -0
- package/dist/components/state-patterns.d.ts +291 -0
- package/dist/components/state-patterns.d.ts.map +1 -0
- package/dist/components/state-patterns.js +970 -0
- package/dist/components/state-patterns.js.map +1 -0
- package/dist/config/config-validation.d.ts +74 -0
- package/dist/config/config-validation.d.ts.map +1 -0
- package/dist/config/config-validation.js +446 -0
- package/dist/config/config-validation.js.map +1 -0
- package/dist/config/default-values.d.ts +72 -0
- package/dist/config/default-values.d.ts.map +1 -0
- package/dist/config/default-values.js +386 -0
- package/dist/config/default-values.js.map +1 -0
- package/dist/config/env-naming.d.ts +73 -0
- package/dist/config/env-naming.d.ts.map +1 -0
- package/dist/config/env-naming.js +429 -0
- package/dist/config/env-naming.js.map +1 -0
- package/dist/config/environment-detection.d.ts +72 -0
- package/dist/config/environment-detection.d.ts.map +1 -0
- package/dist/config/environment-detection.js +400 -0
- package/dist/config/environment-detection.js.map +1 -0
- package/dist/config/feature-flags.d.ts +72 -0
- package/dist/config/feature-flags.d.ts.map +1 -0
- package/dist/config/feature-flags.js +384 -0
- package/dist/config/feature-flags.js.map +1 -0
- package/dist/config/index.d.ts +27 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +43 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/required-optional.d.ts +71 -0
- package/dist/config/required-optional.d.ts.map +1 -0
- package/dist/config/required-optional.js +344 -0
- package/dist/config/required-optional.js.map +1 -0
- package/dist/data-access/connection-pooling.d.ts +63 -0
- package/dist/data-access/connection-pooling.d.ts.map +1 -0
- package/dist/data-access/connection-pooling.js +297 -0
- package/dist/data-access/connection-pooling.js.map +1 -0
- package/dist/data-access/dto-patterns.d.ts +64 -0
- package/dist/data-access/dto-patterns.d.ts.map +1 -0
- package/dist/data-access/dto-patterns.js +291 -0
- package/dist/data-access/dto-patterns.js.map +1 -0
- package/dist/data-access/index.d.ts +31 -0
- package/dist/data-access/index.d.ts.map +1 -0
- package/dist/data-access/index.js +49 -0
- package/dist/data-access/index.js.map +1 -0
- package/dist/data-access/n-plus-one.d.ts +60 -0
- package/dist/data-access/n-plus-one.d.ts.map +1 -0
- package/dist/data-access/n-plus-one.js +264 -0
- package/dist/data-access/n-plus-one.js.map +1 -0
- package/dist/data-access/query-patterns.d.ts +64 -0
- package/dist/data-access/query-patterns.d.ts.map +1 -0
- package/dist/data-access/query-patterns.js +314 -0
- package/dist/data-access/query-patterns.js.map +1 -0
- package/dist/data-access/repository-pattern.d.ts +62 -0
- package/dist/data-access/repository-pattern.d.ts.map +1 -0
- package/dist/data-access/repository-pattern.js +257 -0
- package/dist/data-access/repository-pattern.js.map +1 -0
- package/dist/data-access/transaction-patterns.d.ts +61 -0
- package/dist/data-access/transaction-patterns.d.ts.map +1 -0
- package/dist/data-access/transaction-patterns.js +277 -0
- package/dist/data-access/transaction-patterns.js.map +1 -0
- package/dist/data-access/validation-patterns.d.ts +62 -0
- package/dist/data-access/validation-patterns.d.ts.map +1 -0
- package/dist/data-access/validation-patterns.js +301 -0
- package/dist/data-access/validation-patterns.js.map +1 -0
- package/dist/documentation/deprecation.d.ts +62 -0
- package/dist/documentation/deprecation.d.ts.map +1 -0
- package/dist/documentation/deprecation.js +83 -0
- package/dist/documentation/deprecation.js.map +1 -0
- package/dist/documentation/example-code.d.ts +64 -0
- package/dist/documentation/example-code.d.ts.map +1 -0
- package/dist/documentation/example-code.js +79 -0
- package/dist/documentation/example-code.js.map +1 -0
- package/dist/documentation/index.d.ts +22 -0
- package/dist/documentation/index.d.ts.map +1 -0
- package/dist/documentation/index.js +19 -0
- package/dist/documentation/index.js.map +1 -0
- package/dist/documentation/jsdoc-patterns.d.ts +72 -0
- package/dist/documentation/jsdoc-patterns.d.ts.map +1 -0
- package/dist/documentation/jsdoc-patterns.js +92 -0
- package/dist/documentation/jsdoc-patterns.js.map +1 -0
- package/dist/documentation/readme-structure.d.ts +67 -0
- package/dist/documentation/readme-structure.d.ts.map +1 -0
- package/dist/documentation/readme-structure.js +76 -0
- package/dist/documentation/readme-structure.js.map +1 -0
- package/dist/documentation/todo-patterns.d.ts +67 -0
- package/dist/documentation/todo-patterns.d.ts.map +1 -0
- package/dist/documentation/todo-patterns.js +73 -0
- package/dist/documentation/todo-patterns.js.map +1 -0
- package/dist/errors/async-errors.d.ts +72 -0
- package/dist/errors/async-errors.d.ts.map +1 -0
- package/dist/errors/async-errors.js +214 -0
- package/dist/errors/async-errors.js.map +1 -0
- package/dist/errors/circuit-breaker.d.ts +53 -0
- package/dist/errors/circuit-breaker.d.ts.map +1 -0
- package/dist/errors/circuit-breaker.js +241 -0
- package/dist/errors/circuit-breaker.js.map +1 -0
- package/dist/errors/error-codes.d.ts +73 -0
- package/dist/errors/error-codes.d.ts.map +1 -0
- package/dist/errors/error-codes.js +211 -0
- package/dist/errors/error-codes.js.map +1 -0
- package/dist/errors/error-logging.d.ts +73 -0
- package/dist/errors/error-logging.d.ts.map +1 -0
- package/dist/errors/error-logging.js +256 -0
- package/dist/errors/error-logging.js.map +1 -0
- package/dist/errors/error-propagation.d.ts +73 -0
- package/dist/errors/error-propagation.d.ts.map +1 -0
- package/dist/errors/error-propagation.js +244 -0
- package/dist/errors/error-propagation.js.map +1 -0
- package/dist/errors/exception-hierarchy.d.ts +75 -0
- package/dist/errors/exception-hierarchy.d.ts.map +1 -0
- package/dist/errors/exception-hierarchy.js +259 -0
- package/dist/errors/exception-hierarchy.js.map +1 -0
- package/dist/errors/index.d.ts +31 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +49 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/try-catch-placement.d.ts +73 -0
- package/dist/errors/try-catch-placement.d.ts.map +1 -0
- package/dist/errors/try-catch-placement.js +214 -0
- package/dist/errors/try-catch-placement.js.map +1 -0
- package/dist/index.d.ts +221 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +245 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/context-fields.d.ts +48 -0
- package/dist/logging/context-fields.d.ts.map +1 -0
- package/dist/logging/context-fields.js +160 -0
- package/dist/logging/context-fields.js.map +1 -0
- package/dist/logging/correlation-ids.d.ts +44 -0
- package/dist/logging/correlation-ids.d.ts.map +1 -0
- package/dist/logging/correlation-ids.js +144 -0
- package/dist/logging/correlation-ids.js.map +1 -0
- package/dist/logging/health-checks.d.ts +45 -0
- package/dist/logging/health-checks.d.ts.map +1 -0
- package/dist/logging/health-checks.js +165 -0
- package/dist/logging/health-checks.js.map +1 -0
- package/dist/logging/index.d.ts +31 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +49 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/log-levels.d.ts +46 -0
- package/dist/logging/log-levels.d.ts.map +1 -0
- package/dist/logging/log-levels.js +178 -0
- package/dist/logging/log-levels.js.map +1 -0
- package/dist/logging/metric-naming.d.ts +46 -0
- package/dist/logging/metric-naming.d.ts.map +1 -0
- package/dist/logging/metric-naming.js +157 -0
- package/dist/logging/metric-naming.js.map +1 -0
- package/dist/logging/pii-redaction.d.ts +44 -0
- package/dist/logging/pii-redaction.d.ts.map +1 -0
- package/dist/logging/pii-redaction.js +166 -0
- package/dist/logging/pii-redaction.js.map +1 -0
- package/dist/logging/structured-format.d.ts +53 -0
- package/dist/logging/structured-format.d.ts.map +1 -0
- package/dist/logging/structured-format.js +235 -0
- package/dist/logging/structured-format.js.map +1 -0
- package/dist/performance/bundle-size.d.ts +79 -0
- package/dist/performance/bundle-size.d.ts.map +1 -0
- package/dist/performance/bundle-size.js +276 -0
- package/dist/performance/bundle-size.js.map +1 -0
- package/dist/performance/caching-patterns.d.ts +78 -0
- package/dist/performance/caching-patterns.d.ts.map +1 -0
- package/dist/performance/caching-patterns.js +257 -0
- package/dist/performance/caching-patterns.js.map +1 -0
- package/dist/performance/code-splitting.d.ts +86 -0
- package/dist/performance/code-splitting.d.ts.map +1 -0
- package/dist/performance/code-splitting.js +447 -0
- package/dist/performance/code-splitting.js.map +1 -0
- package/dist/performance/debounce-throttle.d.ts +75 -0
- package/dist/performance/debounce-throttle.d.ts.map +1 -0
- package/dist/performance/debounce-throttle.js +232 -0
- package/dist/performance/debounce-throttle.js.map +1 -0
- package/dist/performance/index.d.ts +28 -0
- package/dist/performance/index.d.ts.map +1 -0
- package/dist/performance/index.js +39 -0
- package/dist/performance/index.js.map +1 -0
- package/dist/performance/lazy-loading.d.ts +75 -0
- package/dist/performance/lazy-loading.d.ts.map +1 -0
- package/dist/performance/lazy-loading.js +233 -0
- package/dist/performance/lazy-loading.js.map +1 -0
- package/dist/performance/memoization.d.ts +75 -0
- package/dist/performance/memoization.d.ts.map +1 -0
- package/dist/performance/memoization.js +251 -0
- package/dist/performance/memoization.js.map +1 -0
- package/dist/registry/detector-registry.d.ts +266 -0
- package/dist/registry/detector-registry.d.ts.map +1 -0
- package/dist/registry/detector-registry.js +526 -0
- package/dist/registry/detector-registry.js.map +1 -0
- package/dist/registry/index.d.ts +10 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +10 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/loader.d.ts +232 -0
- package/dist/registry/loader.d.ts.map +1 -0
- package/dist/registry/loader.js +419 -0
- package/dist/registry/loader.js.map +1 -0
- package/dist/registry/types.d.ts +111 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +19 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/security/csp-headers.d.ts +78 -0
- package/dist/security/csp-headers.d.ts.map +1 -0
- package/dist/security/csp-headers.js +401 -0
- package/dist/security/csp-headers.js.map +1 -0
- package/dist/security/csrf-protection.d.ts +72 -0
- package/dist/security/csrf-protection.d.ts.map +1 -0
- package/dist/security/csrf-protection.js +344 -0
- package/dist/security/csrf-protection.js.map +1 -0
- package/dist/security/index.d.ts +30 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +48 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/input-sanitization.d.ts +74 -0
- package/dist/security/input-sanitization.d.ts.map +1 -0
- package/dist/security/input-sanitization.js +373 -0
- package/dist/security/input-sanitization.js.map +1 -0
- package/dist/security/rate-limiting.d.ts +81 -0
- package/dist/security/rate-limiting.d.ts.map +1 -0
- package/dist/security/rate-limiting.js +535 -0
- package/dist/security/rate-limiting.js.map +1 -0
- package/dist/security/secret-management.d.ts +83 -0
- package/dist/security/secret-management.d.ts.map +1 -0
- package/dist/security/secret-management.js +547 -0
- package/dist/security/secret-management.js.map +1 -0
- package/dist/security/sql-injection.d.ts +76 -0
- package/dist/security/sql-injection.d.ts.map +1 -0
- package/dist/security/sql-injection.js +383 -0
- package/dist/security/sql-injection.js.map +1 -0
- package/dist/security/xss-prevention.d.ts +80 -0
- package/dist/security/xss-prevention.d.ts.map +1 -0
- package/dist/security/xss-prevention.js +416 -0
- package/dist/security/xss-prevention.js.map +1 -0
- package/dist/structural/barrel-exports.d.ts +178 -0
- package/dist/structural/barrel-exports.d.ts.map +1 -0
- package/dist/structural/barrel-exports.js +553 -0
- package/dist/structural/barrel-exports.js.map +1 -0
- package/dist/structural/circular-deps.d.ts +140 -0
- package/dist/structural/circular-deps.d.ts.map +1 -0
- package/dist/structural/circular-deps.js +422 -0
- package/dist/structural/circular-deps.js.map +1 -0
- package/dist/structural/co-location.d.ts +202 -0
- package/dist/structural/co-location.d.ts.map +1 -0
- package/dist/structural/co-location.js +640 -0
- package/dist/structural/co-location.js.map +1 -0
- package/dist/structural/directory-structure.d.ts +151 -0
- package/dist/structural/directory-structure.d.ts.map +1 -0
- package/dist/structural/directory-structure.js +457 -0
- package/dist/structural/directory-structure.js.map +1 -0
- package/dist/structural/file-naming.d.ts +61 -0
- package/dist/structural/file-naming.d.ts.map +1 -0
- package/dist/structural/file-naming.js +231 -0
- package/dist/structural/file-naming.js.map +1 -0
- package/dist/structural/import-ordering.d.ts +212 -0
- package/dist/structural/import-ordering.d.ts.map +1 -0
- package/dist/structural/import-ordering.js +821 -0
- package/dist/structural/import-ordering.js.map +1 -0
- package/dist/structural/index.d.ts +23 -0
- package/dist/structural/index.d.ts.map +1 -0
- package/dist/structural/index.js +26 -0
- package/dist/structural/index.js.map +1 -0
- package/dist/structural/module-boundaries.d.ts +164 -0
- package/dist/structural/module-boundaries.d.ts.map +1 -0
- package/dist/structural/module-boundaries.js +616 -0
- package/dist/structural/module-boundaries.js.map +1 -0
- package/dist/structural/package-boundaries.d.ts +182 -0
- package/dist/structural/package-boundaries.d.ts.map +1 -0
- package/dist/structural/package-boundaries.js +602 -0
- package/dist/structural/package-boundaries.js.map +1 -0
- package/dist/styling/class-naming.d.ts +263 -0
- package/dist/styling/class-naming.d.ts.map +1 -0
- package/dist/styling/class-naming.js +892 -0
- package/dist/styling/class-naming.js.map +1 -0
- package/dist/styling/color-usage.d.ts +213 -0
- package/dist/styling/color-usage.d.ts.map +1 -0
- package/dist/styling/color-usage.js +732 -0
- package/dist/styling/color-usage.js.map +1 -0
- package/dist/styling/design-tokens.d.ts +212 -0
- package/dist/styling/design-tokens.d.ts.map +1 -0
- package/dist/styling/design-tokens.js +748 -0
- package/dist/styling/design-tokens.js.map +1 -0
- package/dist/styling/index.d.ts +16 -0
- package/dist/styling/index.d.ts.map +1 -0
- package/dist/styling/index.js +56 -0
- package/dist/styling/index.js.map +1 -0
- package/dist/styling/responsive.d.ts +304 -0
- package/dist/styling/responsive.d.ts.map +1 -0
- package/dist/styling/responsive.js +888 -0
- package/dist/styling/responsive.js.map +1 -0
- package/dist/styling/spacing-scale.d.ts +248 -0
- package/dist/styling/spacing-scale.d.ts.map +1 -0
- package/dist/styling/spacing-scale.js +865 -0
- package/dist/styling/spacing-scale.js.map +1 -0
- package/dist/styling/tailwind-patterns.d.ts +305 -0
- package/dist/styling/tailwind-patterns.d.ts.map +1 -0
- package/dist/styling/tailwind-patterns.js +1181 -0
- package/dist/styling/tailwind-patterns.js.map +1 -0
- package/dist/styling/typography.d.ts +281 -0
- package/dist/styling/typography.d.ts.map +1 -0
- package/dist/styling/typography.js +1004 -0
- package/dist/styling/typography.js.map +1 -0
- package/dist/styling/z-index-scale.d.ts +270 -0
- package/dist/styling/z-index-scale.d.ts.map +1 -0
- package/dist/styling/z-index-scale.js +714 -0
- package/dist/styling/z-index-scale.js.map +1 -0
- package/dist/testing/co-location.d.ts +42 -0
- package/dist/testing/co-location.d.ts.map +1 -0
- package/dist/testing/co-location.js +134 -0
- package/dist/testing/co-location.js.map +1 -0
- package/dist/testing/describe-naming.d.ts +47 -0
- package/dist/testing/describe-naming.d.ts.map +1 -0
- package/dist/testing/describe-naming.js +150 -0
- package/dist/testing/describe-naming.js.map +1 -0
- package/dist/testing/file-naming.d.ts +44 -0
- package/dist/testing/file-naming.d.ts.map +1 -0
- package/dist/testing/file-naming.js +131 -0
- package/dist/testing/file-naming.js.map +1 -0
- package/dist/testing/fixture-patterns.d.ts +52 -0
- package/dist/testing/fixture-patterns.d.ts.map +1 -0
- package/dist/testing/fixture-patterns.js +228 -0
- package/dist/testing/fixture-patterns.js.map +1 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +49 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-patterns.d.ts +53 -0
- package/dist/testing/mock-patterns.d.ts.map +1 -0
- package/dist/testing/mock-patterns.js +264 -0
- package/dist/testing/mock-patterns.js.map +1 -0
- package/dist/testing/setup-teardown.d.ts +55 -0
- package/dist/testing/setup-teardown.d.ts.map +1 -0
- package/dist/testing/setup-teardown.js +262 -0
- package/dist/testing/setup-teardown.js.map +1 -0
- package/dist/testing/test-structure.d.ts +51 -0
- package/dist/testing/test-structure.d.ts.map +1 -0
- package/dist/testing/test-structure.js +225 -0
- package/dist/testing/test-structure.js.map +1 -0
- package/dist/types/any-usage.d.ts +99 -0
- package/dist/types/any-usage.d.ts.map +1 -0
- package/dist/types/any-usage.js +641 -0
- package/dist/types/any-usage.js.map +1 -0
- package/dist/types/file-location.d.ts +76 -0
- package/dist/types/file-location.d.ts.map +1 -0
- package/dist/types/file-location.js +395 -0
- package/dist/types/file-location.js.map +1 -0
- package/dist/types/generic-patterns.d.ts +97 -0
- package/dist/types/generic-patterns.d.ts.map +1 -0
- package/dist/types/generic-patterns.js +615 -0
- package/dist/types/generic-patterns.js.map +1 -0
- package/dist/types/index.d.ts +31 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +43 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/interface-vs-type.d.ts +81 -0
- package/dist/types/interface-vs-type.d.ts.map +1 -0
- package/dist/types/interface-vs-type.js +440 -0
- package/dist/types/interface-vs-type.js.map +1 -0
- package/dist/types/naming-conventions.d.ts +84 -0
- package/dist/types/naming-conventions.d.ts.map +1 -0
- package/dist/types/naming-conventions.js +455 -0
- package/dist/types/naming-conventions.js.map +1 -0
- package/dist/types/type-assertions.d.ts +98 -0
- package/dist/types/type-assertions.d.ts.map +1 -0
- package/dist/types/type-assertions.js +639 -0
- package/dist/types/type-assertions.js.map +1 -0
- package/dist/types/utility-types.d.ts +110 -0
- package/dist/types/utility-types.d.ts.map +1 -0
- package/dist/types/utility-types.js +547 -0
- package/dist/types/utility-types.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Near Duplicate Detector - Semantic similarity detection for abstraction candidates
|
|
3
|
+
*
|
|
4
|
+
* Detects semantically similar components that could be refactored into shared
|
|
5
|
+
* abstractions. Unlike duplicate-detection.ts which focuses on AST structure,
|
|
6
|
+
* this detector analyzes functional similarity including:
|
|
7
|
+
* - Similar prop patterns
|
|
8
|
+
* - Similar render patterns
|
|
9
|
+
* - Similar state management
|
|
10
|
+
* - Opportunities for shared hooks, HOCs, or render props
|
|
11
|
+
*
|
|
12
|
+
* @requirements 8.4 - THE Component_Detector SHALL detect near-duplicate components that should be abstracted
|
|
13
|
+
*/
|
|
14
|
+
import { ASTDetector } from '../base/index.js';
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Constants
|
|
17
|
+
// ============================================================================
|
|
18
|
+
/**
|
|
19
|
+
* Default configuration for near-duplicate detection
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_NEAR_DUPLICATE_CONFIG = {
|
|
22
|
+
similarityThreshold: 0.6,
|
|
23
|
+
minPropsSimilarity: 0.5,
|
|
24
|
+
minJsxSimilarity: 0.4,
|
|
25
|
+
detectHookOpportunities: true,
|
|
26
|
+
detectHOCOpportunities: true,
|
|
27
|
+
weights: {
|
|
28
|
+
props: 0.25,
|
|
29
|
+
state: 0.15,
|
|
30
|
+
hooks: 0.15,
|
|
31
|
+
eventHandlers: 0.1,
|
|
32
|
+
jsxStructure: 0.2,
|
|
33
|
+
conditionalPatterns: 0.1,
|
|
34
|
+
dataPatterns: 0.05,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Built-in React hooks
|
|
39
|
+
*/
|
|
40
|
+
export const REACT_HOOKS = new Set([
|
|
41
|
+
'useState',
|
|
42
|
+
'useEffect',
|
|
43
|
+
'useContext',
|
|
44
|
+
'useReducer',
|
|
45
|
+
'useCallback',
|
|
46
|
+
'useMemo',
|
|
47
|
+
'useRef',
|
|
48
|
+
'useImperativeHandle',
|
|
49
|
+
'useLayoutEffect',
|
|
50
|
+
'useDebugValue',
|
|
51
|
+
'useDeferredValue',
|
|
52
|
+
'useTransition',
|
|
53
|
+
'useId',
|
|
54
|
+
'useSyncExternalStore',
|
|
55
|
+
'useInsertionEffect',
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* Common event handler patterns
|
|
59
|
+
*/
|
|
60
|
+
export const EVENT_HANDLER_PATTERNS = [
|
|
61
|
+
'onClick',
|
|
62
|
+
'onChange',
|
|
63
|
+
'onSubmit',
|
|
64
|
+
'onBlur',
|
|
65
|
+
'onFocus',
|
|
66
|
+
'onKeyDown',
|
|
67
|
+
'onKeyUp',
|
|
68
|
+
'onKeyPress',
|
|
69
|
+
'onMouseEnter',
|
|
70
|
+
'onMouseLeave',
|
|
71
|
+
'onScroll',
|
|
72
|
+
'onLoad',
|
|
73
|
+
'onError',
|
|
74
|
+
];
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Helper Functions - Feature Extraction
|
|
77
|
+
// ============================================================================
|
|
78
|
+
/**
|
|
79
|
+
* Check if a node represents a React component
|
|
80
|
+
*/
|
|
81
|
+
export function isReactComponentNode(node, content) {
|
|
82
|
+
if (node.type === 'function_declaration' ||
|
|
83
|
+
node.type === 'arrow_function' ||
|
|
84
|
+
node.type === 'function_expression') {
|
|
85
|
+
const name = extractComponentName(node, content);
|
|
86
|
+
if (!name || !/^[A-Z]/.test(name)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const nodeText = node.text;
|
|
90
|
+
return nodeText.includes('<') && (nodeText.includes('/>') || nodeText.includes('</'));
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Extract component name from an AST node
|
|
96
|
+
*/
|
|
97
|
+
export function extractComponentName(node, content) {
|
|
98
|
+
if (node.type === 'function_declaration') {
|
|
99
|
+
const nameNode = node.children.find(c => c.type === 'identifier');
|
|
100
|
+
return nameNode?.text;
|
|
101
|
+
}
|
|
102
|
+
const lines = content.split('\n');
|
|
103
|
+
const line = lines[node.startPosition.row];
|
|
104
|
+
if (line) {
|
|
105
|
+
const match = line.match(/(?:const|let|var|export\s+(?:const|let|var)?)\s+([A-Z][a-zA-Z0-9]*)\s*[=:]/);
|
|
106
|
+
if (match && match[1]) {
|
|
107
|
+
return match[1];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Extract props from a component
|
|
114
|
+
*/
|
|
115
|
+
export function extractProps(node, _content) {
|
|
116
|
+
const props = [];
|
|
117
|
+
const nodeText = node.text;
|
|
118
|
+
// Extract from destructuring pattern: ({ prop1, prop2 = 'default', ...rest })
|
|
119
|
+
const destructuringMatch = nodeText.match(/\(\s*\{\s*([^}]+)\s*\}/);
|
|
120
|
+
if (destructuringMatch && destructuringMatch[1]) {
|
|
121
|
+
const propsStr = destructuringMatch[1];
|
|
122
|
+
const propParts = propsStr.split(',');
|
|
123
|
+
for (const part of propParts) {
|
|
124
|
+
const trimmed = part.trim();
|
|
125
|
+
if (trimmed.startsWith('...'))
|
|
126
|
+
continue; // Skip rest spread
|
|
127
|
+
// Match: propName, propName = default, propName: type
|
|
128
|
+
const propMatch = trimmed.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:=\s*([^,]+))?/);
|
|
129
|
+
if (propMatch && propMatch[1]) {
|
|
130
|
+
const propName = propMatch[1];
|
|
131
|
+
const hasDefault = !!propMatch[2];
|
|
132
|
+
props.push({
|
|
133
|
+
name: propName,
|
|
134
|
+
hasDefault,
|
|
135
|
+
isRequired: !hasDefault,
|
|
136
|
+
isCallback: propName.startsWith('on') || propName.startsWith('handle'),
|
|
137
|
+
isChildren: propName === 'children',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Also check for props.propName access
|
|
143
|
+
const directAccessMatches = nodeText.matchAll(/props\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g);
|
|
144
|
+
for (const match of directAccessMatches) {
|
|
145
|
+
if (match[1] && !props.some(p => p.name === match[1])) {
|
|
146
|
+
props.push({
|
|
147
|
+
name: match[1],
|
|
148
|
+
hasDefault: false,
|
|
149
|
+
isRequired: true,
|
|
150
|
+
isCallback: match[1].startsWith('on') || match[1].startsWith('handle'),
|
|
151
|
+
isChildren: match[1] === 'children',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return props;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Extract state variables from a component
|
|
159
|
+
*/
|
|
160
|
+
export function extractState(node) {
|
|
161
|
+
const state = [];
|
|
162
|
+
const nodeText = node.text;
|
|
163
|
+
// Match useState: const [value, setValue] = useState(initial)
|
|
164
|
+
const useStateMatches = nodeText.matchAll(/const\s+\[\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\]\s*=\s*useState\s*(?:<[^>]*>)?\s*\(([^)]*)\)/g);
|
|
165
|
+
for (const match of useStateMatches) {
|
|
166
|
+
if (match[1] && match[2]) {
|
|
167
|
+
const feature = {
|
|
168
|
+
name: match[1],
|
|
169
|
+
setter: match[2],
|
|
170
|
+
stateType: 'useState',
|
|
171
|
+
};
|
|
172
|
+
const initialValue = match[3]?.trim();
|
|
173
|
+
if (initialValue) {
|
|
174
|
+
feature.initialValue = initialValue;
|
|
175
|
+
}
|
|
176
|
+
state.push(feature);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Match useReducer: const [state, dispatch] = useReducer(reducer, initial)
|
|
180
|
+
const useReducerMatches = nodeText.matchAll(/const\s+\[\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\]\s*=\s*useReducer/g);
|
|
181
|
+
for (const match of useReducerMatches) {
|
|
182
|
+
if (match[1] && match[2]) {
|
|
183
|
+
state.push({
|
|
184
|
+
name: match[1],
|
|
185
|
+
setter: match[2],
|
|
186
|
+
stateType: 'useReducer',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Match useRef: const ref = useRef(initial)
|
|
191
|
+
const useRefMatches = nodeText.matchAll(/const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*useRef\s*(?:<[^>]*>)?\s*\(([^)]*)\)/g);
|
|
192
|
+
for (const match of useRefMatches) {
|
|
193
|
+
if (match[1]) {
|
|
194
|
+
const feature = {
|
|
195
|
+
name: match[1],
|
|
196
|
+
setter: `${match[1]}.current`,
|
|
197
|
+
stateType: 'useRef',
|
|
198
|
+
};
|
|
199
|
+
const initialValue = match[2]?.trim();
|
|
200
|
+
if (initialValue) {
|
|
201
|
+
feature.initialValue = initialValue;
|
|
202
|
+
}
|
|
203
|
+
state.push(feature);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return state;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Extract hooks used in a component
|
|
210
|
+
*/
|
|
211
|
+
export function extractHooks(node) {
|
|
212
|
+
const hooks = [];
|
|
213
|
+
const nodeText = node.text;
|
|
214
|
+
// Match hook calls: useXxx(...) or use_xxx(...)
|
|
215
|
+
const hookMatches = nodeText.matchAll(/\b(use[A-Z][a-zA-Z0-9]*|use_[a-z][a-zA-Z0-9_]*)\s*\(/g);
|
|
216
|
+
const seenHooks = new Set();
|
|
217
|
+
for (const match of hookMatches) {
|
|
218
|
+
if (match[1] && !seenHooks.has(match[1])) {
|
|
219
|
+
seenHooks.add(match[1]);
|
|
220
|
+
const hookName = match[1];
|
|
221
|
+
const isBuiltIn = REACT_HOOKS.has(hookName);
|
|
222
|
+
const feature = {
|
|
223
|
+
name: hookName,
|
|
224
|
+
isBuiltIn,
|
|
225
|
+
};
|
|
226
|
+
// Try to extract dependencies for effect-like hooks
|
|
227
|
+
if (['useEffect', 'useCallback', 'useMemo', 'useLayoutEffect'].includes(hookName)) {
|
|
228
|
+
const depMatch = nodeText.match(new RegExp(`${hookName}\\s*\\([^)]*,\\s*\\[([^\\]]*)\\]`));
|
|
229
|
+
if (depMatch && depMatch[1]) {
|
|
230
|
+
feature.dependencies = depMatch[1].split(',').map(d => d.trim()).filter(d => d.length > 0);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
hooks.push(feature);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return hooks;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Extract event handlers from a component
|
|
240
|
+
*/
|
|
241
|
+
export function extractEventHandlers(node) {
|
|
242
|
+
const handlers = [];
|
|
243
|
+
const nodeText = node.text;
|
|
244
|
+
const seenHandlers = new Set();
|
|
245
|
+
// Match event handler props: onClick={handleClick} or onClick={() => ...}
|
|
246
|
+
for (const eventType of EVENT_HANDLER_PATTERNS) {
|
|
247
|
+
const handlerMatches = nodeText.matchAll(new RegExp(`${eventType}\\s*=\\s*\\{([^}]+)\\}`, 'g'));
|
|
248
|
+
for (const match of handlerMatches) {
|
|
249
|
+
if (match[1]) {
|
|
250
|
+
const handlerContent = match[1].trim();
|
|
251
|
+
const isInline = handlerContent.includes('=>') || handlerContent.includes('function');
|
|
252
|
+
const handlerName = isInline ? `inline_${eventType}` : handlerContent;
|
|
253
|
+
if (!seenHandlers.has(`${eventType}_${handlerName}`)) {
|
|
254
|
+
seenHandlers.add(`${eventType}_${handlerName}`);
|
|
255
|
+
handlers.push({
|
|
256
|
+
name: handlerName,
|
|
257
|
+
eventType,
|
|
258
|
+
isInline,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Match handler function definitions: const handleClick = ...
|
|
265
|
+
const handlerDefMatches = nodeText.matchAll(/const\s+(handle[A-Z][a-zA-Z0-9]*|on[A-Z][a-zA-Z0-9]*)\s*=/g);
|
|
266
|
+
for (const match of handlerDefMatches) {
|
|
267
|
+
if (match[1] && !seenHandlers.has(match[1])) {
|
|
268
|
+
seenHandlers.add(match[1]);
|
|
269
|
+
handlers.push({
|
|
270
|
+
name: match[1],
|
|
271
|
+
eventType: 'custom',
|
|
272
|
+
isInline: false,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return handlers;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Extract JSX elements from a component
|
|
280
|
+
*/
|
|
281
|
+
export function extractJSXElements(node) {
|
|
282
|
+
const elements = [];
|
|
283
|
+
const nodeText = node.text;
|
|
284
|
+
// Match JSX opening tags: <TagName prop1={...} prop2="...">
|
|
285
|
+
const jsxMatches = nodeText.matchAll(/<([A-Za-z][A-Za-z0-9.]*)\s*([^>]*?)(?:\/?>)/g);
|
|
286
|
+
for (const match of jsxMatches) {
|
|
287
|
+
if (match[1]) {
|
|
288
|
+
const tagName = match[1];
|
|
289
|
+
const propsStr = match[2] || '';
|
|
290
|
+
const isComponent = /^[A-Z]/.test(tagName);
|
|
291
|
+
// Extract prop names from the props string
|
|
292
|
+
const propMatches = propsStr.matchAll(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g);
|
|
293
|
+
const props = [];
|
|
294
|
+
for (const propMatch of propMatches) {
|
|
295
|
+
if (propMatch[1]) {
|
|
296
|
+
props.push(propMatch[1]);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
elements.push({
|
|
300
|
+
tagName,
|
|
301
|
+
isComponent,
|
|
302
|
+
props,
|
|
303
|
+
depth: 0, // Simplified - would need proper parsing for accurate depth
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return elements;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Extract conditional rendering patterns from a component
|
|
311
|
+
*/
|
|
312
|
+
export function extractConditionalPatterns(node) {
|
|
313
|
+
const patterns = [];
|
|
314
|
+
const nodeText = node.text;
|
|
315
|
+
// Match ternary in JSX: {condition ? <A /> : <B />}
|
|
316
|
+
const ternaryMatches = nodeText.matchAll(/\{\s*([a-zA-Z_$][a-zA-Z0-9_$?.]*)\s*\?\s*</g);
|
|
317
|
+
for (const match of ternaryMatches) {
|
|
318
|
+
if (match[1]) {
|
|
319
|
+
patterns.push({
|
|
320
|
+
type: 'ternary',
|
|
321
|
+
condition: match[1],
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Match logical AND: {condition && <Component />}
|
|
326
|
+
const logicalAndMatches = nodeText.matchAll(/\{\s*([a-zA-Z_$][a-zA-Z0-9_$?.]*)\s*&&\s*</g);
|
|
327
|
+
for (const match of logicalAndMatches) {
|
|
328
|
+
if (match[1]) {
|
|
329
|
+
patterns.push({
|
|
330
|
+
type: 'logical-and',
|
|
331
|
+
condition: match[1],
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Match early returns: if (!condition) return null;
|
|
336
|
+
const earlyReturnMatches = nodeText.matchAll(/if\s*\(\s*!?\s*([a-zA-Z_$][a-zA-Z0-9_$?.]*)\s*\)\s*return\s*null/g);
|
|
337
|
+
for (const match of earlyReturnMatches) {
|
|
338
|
+
if (match[1]) {
|
|
339
|
+
patterns.push({
|
|
340
|
+
type: 'early-return',
|
|
341
|
+
condition: match[1],
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return patterns;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Extract data fetching patterns from a component
|
|
349
|
+
*/
|
|
350
|
+
export function extractDataPatterns(node) {
|
|
351
|
+
const patterns = [];
|
|
352
|
+
const nodeText = node.text;
|
|
353
|
+
// Check for useQuery (React Query)
|
|
354
|
+
if (nodeText.includes('useQuery')) {
|
|
355
|
+
patterns.push({
|
|
356
|
+
type: 'useQuery',
|
|
357
|
+
hasLoadingState: nodeText.includes('isLoading') || nodeText.includes('loading'),
|
|
358
|
+
hasErrorState: nodeText.includes('isError') || nodeText.includes('error'),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
// Check for useSWR
|
|
362
|
+
if (nodeText.includes('useSWR')) {
|
|
363
|
+
patterns.push({
|
|
364
|
+
type: 'useSWR',
|
|
365
|
+
hasLoadingState: nodeText.includes('isLoading') || nodeText.includes('isValidating'),
|
|
366
|
+
hasErrorState: nodeText.includes('error'),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
// Check for useEffect with fetch
|
|
370
|
+
if (nodeText.includes('useEffect') && (nodeText.includes('fetch(') || nodeText.includes('axios'))) {
|
|
371
|
+
patterns.push({
|
|
372
|
+
type: 'useEffect-fetch',
|
|
373
|
+
hasLoadingState: nodeText.includes('Loading') || nodeText.includes('loading'),
|
|
374
|
+
hasErrorState: nodeText.includes('Error') || nodeText.includes('error'),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return patterns;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Extract all semantic features from a component
|
|
381
|
+
*/
|
|
382
|
+
export function extractSemanticFeatures(node, filePath, content) {
|
|
383
|
+
const name = extractComponentName(node, content);
|
|
384
|
+
if (!name) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
name,
|
|
389
|
+
filePath,
|
|
390
|
+
line: node.startPosition.row + 1,
|
|
391
|
+
column: node.startPosition.column + 1,
|
|
392
|
+
props: extractProps(node, content),
|
|
393
|
+
stateVariables: extractState(node),
|
|
394
|
+
hooks: extractHooks(node),
|
|
395
|
+
eventHandlers: extractEventHandlers(node),
|
|
396
|
+
jsxElements: extractJSXElements(node),
|
|
397
|
+
conditionalPatterns: extractConditionalPatterns(node),
|
|
398
|
+
dataPatterns: extractDataPatterns(node),
|
|
399
|
+
sourceCode: node.text,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// ============================================================================
|
|
403
|
+
// Helper Functions - Similarity Calculation
|
|
404
|
+
// ============================================================================
|
|
405
|
+
/**
|
|
406
|
+
* Calculate Jaccard similarity between two sets
|
|
407
|
+
*/
|
|
408
|
+
function jaccardSimilarity(set1, set2) {
|
|
409
|
+
if (set1.size === 0 && set2.size === 0) {
|
|
410
|
+
return 1.0;
|
|
411
|
+
}
|
|
412
|
+
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
413
|
+
const union = new Set([...set1, ...set2]);
|
|
414
|
+
return intersection.size / union.size;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Calculate props similarity between two components
|
|
418
|
+
*/
|
|
419
|
+
export function calculatePropsSimilarity(props1, props2) {
|
|
420
|
+
if (props1.length === 0 && props2.length === 0) {
|
|
421
|
+
return 1.0;
|
|
422
|
+
}
|
|
423
|
+
const names1 = new Set(props1.map(p => p.name));
|
|
424
|
+
const names2 = new Set(props2.map(p => p.name));
|
|
425
|
+
const nameSimilarity = jaccardSimilarity(names1, names2);
|
|
426
|
+
// Also consider prop characteristics
|
|
427
|
+
const callbacks1 = new Set(props1.filter(p => p.isCallback).map(p => p.name));
|
|
428
|
+
const callbacks2 = new Set(props2.filter(p => p.isCallback).map(p => p.name));
|
|
429
|
+
const callbackSimilarity = jaccardSimilarity(callbacks1, callbacks2);
|
|
430
|
+
return nameSimilarity * 0.7 + callbackSimilarity * 0.3;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Calculate state similarity between two components
|
|
434
|
+
*/
|
|
435
|
+
export function calculateStateSimilarity(state1, state2) {
|
|
436
|
+
if (state1.length === 0 && state2.length === 0) {
|
|
437
|
+
return 1.0;
|
|
438
|
+
}
|
|
439
|
+
// Compare state types
|
|
440
|
+
const types1 = new Set(state1.map(s => s.stateType));
|
|
441
|
+
const types2 = new Set(state2.map(s => s.stateType));
|
|
442
|
+
const typeSimilarity = jaccardSimilarity(types1, types2);
|
|
443
|
+
// Compare number of state variables
|
|
444
|
+
const countDiff = Math.abs(state1.length - state2.length);
|
|
445
|
+
const maxCount = Math.max(state1.length, state2.length, 1);
|
|
446
|
+
const countSimilarity = 1 - (countDiff / maxCount);
|
|
447
|
+
return typeSimilarity * 0.6 + countSimilarity * 0.4;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Calculate hooks similarity between two components
|
|
451
|
+
*/
|
|
452
|
+
export function calculateHooksSimilarity(hooks1, hooks2) {
|
|
453
|
+
if (hooks1.length === 0 && hooks2.length === 0) {
|
|
454
|
+
return 1.0;
|
|
455
|
+
}
|
|
456
|
+
const names1 = new Set(hooks1.map(h => h.name));
|
|
457
|
+
const names2 = new Set(hooks2.map(h => h.name));
|
|
458
|
+
return jaccardSimilarity(names1, names2);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Calculate event handlers similarity between two components
|
|
462
|
+
*/
|
|
463
|
+
export function calculateEventHandlersSimilarity(handlers1, handlers2) {
|
|
464
|
+
if (handlers1.length === 0 && handlers2.length === 0) {
|
|
465
|
+
return 1.0;
|
|
466
|
+
}
|
|
467
|
+
const types1 = new Set(handlers1.map(h => h.eventType));
|
|
468
|
+
const types2 = new Set(handlers2.map(h => h.eventType));
|
|
469
|
+
return jaccardSimilarity(types1, types2);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Calculate JSX structure similarity between two components
|
|
473
|
+
*/
|
|
474
|
+
export function calculateJSXSimilarity(elements1, elements2) {
|
|
475
|
+
if (elements1.length === 0 && elements2.length === 0) {
|
|
476
|
+
return 1.0;
|
|
477
|
+
}
|
|
478
|
+
// Compare tag names
|
|
479
|
+
const tags1 = new Set(elements1.map(e => e.tagName));
|
|
480
|
+
const tags2 = new Set(elements2.map(e => e.tagName));
|
|
481
|
+
const tagSimilarity = jaccardSimilarity(tags1, tags2);
|
|
482
|
+
// Compare component usage
|
|
483
|
+
const components1 = new Set(elements1.filter(e => e.isComponent).map(e => e.tagName));
|
|
484
|
+
const components2 = new Set(elements2.filter(e => e.isComponent).map(e => e.tagName));
|
|
485
|
+
const componentSimilarity = jaccardSimilarity(components1, components2);
|
|
486
|
+
// Compare element count
|
|
487
|
+
const countDiff = Math.abs(elements1.length - elements2.length);
|
|
488
|
+
const maxCount = Math.max(elements1.length, elements2.length, 1);
|
|
489
|
+
const countSimilarity = 1 - (countDiff / maxCount);
|
|
490
|
+
return tagSimilarity * 0.4 + componentSimilarity * 0.4 + countSimilarity * 0.2;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Calculate conditional patterns similarity
|
|
494
|
+
*/
|
|
495
|
+
export function calculateConditionalSimilarity(patterns1, patterns2) {
|
|
496
|
+
if (patterns1.length === 0 && patterns2.length === 0) {
|
|
497
|
+
return 1.0;
|
|
498
|
+
}
|
|
499
|
+
const types1 = new Set(patterns1.map(p => p.type));
|
|
500
|
+
const types2 = new Set(patterns2.map(p => p.type));
|
|
501
|
+
return jaccardSimilarity(types1, types2);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Calculate data patterns similarity
|
|
505
|
+
*/
|
|
506
|
+
export function calculateDataPatternsSimilarity(patterns1, patterns2) {
|
|
507
|
+
if (patterns1.length === 0 && patterns2.length === 0) {
|
|
508
|
+
return 1.0;
|
|
509
|
+
}
|
|
510
|
+
const types1 = new Set(patterns1.map(p => p.type));
|
|
511
|
+
const types2 = new Set(patterns2.map(p => p.type));
|
|
512
|
+
return jaccardSimilarity(types1, types2);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Calculate overall semantic similarity between two components
|
|
516
|
+
*/
|
|
517
|
+
export function calculateSemanticSimilarity(comp1, comp2) {
|
|
518
|
+
return {
|
|
519
|
+
props: calculatePropsSimilarity(comp1.props, comp2.props),
|
|
520
|
+
state: calculateStateSimilarity(comp1.stateVariables, comp2.stateVariables),
|
|
521
|
+
hooks: calculateHooksSimilarity(comp1.hooks, comp2.hooks),
|
|
522
|
+
eventHandlers: calculateEventHandlersSimilarity(comp1.eventHandlers, comp2.eventHandlers),
|
|
523
|
+
jsxStructure: calculateJSXSimilarity(comp1.jsxElements, comp2.jsxElements),
|
|
524
|
+
conditionalPatterns: calculateConditionalSimilarity(comp1.conditionalPatterns, comp2.conditionalPatterns),
|
|
525
|
+
dataPatterns: calculateDataPatternsSimilarity(comp1.dataPatterns, comp2.dataPatterns),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Calculate weighted overall similarity from breakdown
|
|
530
|
+
*/
|
|
531
|
+
export function calculateOverallSimilarity(breakdown, weights = DEFAULT_NEAR_DUPLICATE_CONFIG.weights) {
|
|
532
|
+
const totalWeight = Object.values(weights).reduce((sum, w) => sum + w, 0);
|
|
533
|
+
const weightedSum = breakdown.props * weights.props +
|
|
534
|
+
breakdown.state * weights.state +
|
|
535
|
+
breakdown.hooks * weights.hooks +
|
|
536
|
+
breakdown.eventHandlers * weights.eventHandlers +
|
|
537
|
+
breakdown.jsxStructure * weights.jsxStructure +
|
|
538
|
+
breakdown.conditionalPatterns * weights.conditionalPatterns +
|
|
539
|
+
breakdown.dataPatterns * weights.dataPatterns;
|
|
540
|
+
return weightedSum / totalWeight;
|
|
541
|
+
}
|
|
542
|
+
// ============================================================================
|
|
543
|
+
// Helper Functions - Abstraction Suggestions
|
|
544
|
+
// ============================================================================
|
|
545
|
+
/**
|
|
546
|
+
* Determine the best abstraction type for a pair of similar components
|
|
547
|
+
*/
|
|
548
|
+
export function determineAbstractionType(comp1, comp2, breakdown) {
|
|
549
|
+
// If hooks are very similar and there's shared state logic, suggest shared hook
|
|
550
|
+
if (breakdown.hooks > 0.7 && breakdown.state > 0.6) {
|
|
551
|
+
const customHooks1 = comp1.hooks.filter(h => !h.isBuiltIn);
|
|
552
|
+
const customHooks2 = comp2.hooks.filter(h => !h.isBuiltIn);
|
|
553
|
+
if (customHooks1.length === 0 && customHooks2.length === 0 && comp1.stateVariables.length > 0) {
|
|
554
|
+
return 'shared-hook';
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// If JSX structure is very similar with different props, suggest shared component
|
|
558
|
+
if (breakdown.jsxStructure > 0.7 && breakdown.props > 0.5) {
|
|
559
|
+
return 'shared-component';
|
|
560
|
+
}
|
|
561
|
+
// If there's similar conditional/data patterns, suggest HOC
|
|
562
|
+
if (breakdown.conditionalPatterns > 0.7 || breakdown.dataPatterns > 0.7) {
|
|
563
|
+
return 'higher-order';
|
|
564
|
+
}
|
|
565
|
+
// If event handlers are similar, suggest composition
|
|
566
|
+
if (breakdown.eventHandlers > 0.7) {
|
|
567
|
+
return 'composition';
|
|
568
|
+
}
|
|
569
|
+
// Default to shared component
|
|
570
|
+
return 'shared-component';
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Generate abstraction suggestions for a pair of similar components
|
|
574
|
+
*/
|
|
575
|
+
export function generateAbstractionSuggestions(comp1, comp2, breakdown, abstractionType) {
|
|
576
|
+
const suggestions = [];
|
|
577
|
+
// Find shared features
|
|
578
|
+
const sharedProps = comp1.props
|
|
579
|
+
.filter(p1 => comp2.props.some(p2 => p2.name === p1.name))
|
|
580
|
+
.map(p => p.name);
|
|
581
|
+
const sharedHooks = comp1.hooks
|
|
582
|
+
.filter(h1 => comp2.hooks.some(h2 => h2.name === h1.name))
|
|
583
|
+
.map(h => h.name);
|
|
584
|
+
const sharedElements = comp1.jsxElements
|
|
585
|
+
.filter(e1 => comp2.jsxElements.some(e2 => e2.tagName === e1.tagName))
|
|
586
|
+
.map(e => e.tagName);
|
|
587
|
+
switch (abstractionType) {
|
|
588
|
+
case 'shared-component':
|
|
589
|
+
suggestions.push({
|
|
590
|
+
type: 'shared-component',
|
|
591
|
+
description: `Create a shared component that accepts variant props to handle differences between '${comp1.name}' and '${comp2.name}'`,
|
|
592
|
+
confidence: breakdown.jsxStructure,
|
|
593
|
+
sharedFeatures: [...sharedProps, ...sharedElements],
|
|
594
|
+
exampleCode: generateSharedComponentExample(comp1, comp2, sharedProps),
|
|
595
|
+
});
|
|
596
|
+
break;
|
|
597
|
+
case 'shared-hook':
|
|
598
|
+
suggestions.push({
|
|
599
|
+
type: 'shared-hook',
|
|
600
|
+
description: `Extract shared state logic into a custom hook that can be used by both '${comp1.name}' and '${comp2.name}'`,
|
|
601
|
+
confidence: breakdown.hooks * breakdown.state,
|
|
602
|
+
sharedFeatures: sharedHooks,
|
|
603
|
+
exampleCode: generateSharedHookExample(comp1, comp2),
|
|
604
|
+
});
|
|
605
|
+
break;
|
|
606
|
+
case 'higher-order':
|
|
607
|
+
suggestions.push({
|
|
608
|
+
type: 'higher-order',
|
|
609
|
+
description: `Create a Higher-Order Component (HOC) to wrap shared behavior for '${comp1.name}' and '${comp2.name}'`,
|
|
610
|
+
confidence: Math.max(breakdown.conditionalPatterns, breakdown.dataPatterns),
|
|
611
|
+
sharedFeatures: [...sharedHooks, ...sharedProps],
|
|
612
|
+
});
|
|
613
|
+
break;
|
|
614
|
+
case 'composition':
|
|
615
|
+
suggestions.push({
|
|
616
|
+
type: 'composition',
|
|
617
|
+
description: `Use composition pattern to share common elements between '${comp1.name}' and '${comp2.name}'`,
|
|
618
|
+
confidence: breakdown.jsxStructure,
|
|
619
|
+
sharedFeatures: sharedElements,
|
|
620
|
+
});
|
|
621
|
+
break;
|
|
622
|
+
case 'render-props':
|
|
623
|
+
suggestions.push({
|
|
624
|
+
type: 'render-props',
|
|
625
|
+
description: `Use render props pattern to share logic while allowing different rendering`,
|
|
626
|
+
confidence: breakdown.state * breakdown.hooks,
|
|
627
|
+
sharedFeatures: [...sharedHooks, ...sharedProps],
|
|
628
|
+
});
|
|
629
|
+
break;
|
|
630
|
+
case 'utility-function':
|
|
631
|
+
suggestions.push({
|
|
632
|
+
type: 'utility-function',
|
|
633
|
+
description: `Extract shared logic into utility functions`,
|
|
634
|
+
confidence: 0.5,
|
|
635
|
+
sharedFeatures: sharedHooks,
|
|
636
|
+
});
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
return suggestions;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Generate example code for shared component suggestion
|
|
643
|
+
*/
|
|
644
|
+
function generateSharedComponentExample(comp1, comp2, sharedProps) {
|
|
645
|
+
const baseName = findCommonBaseName(comp1.name, comp2.name);
|
|
646
|
+
const propsStr = sharedProps.length > 0
|
|
647
|
+
? `{ ${sharedProps.join(', ')}, variant }`
|
|
648
|
+
: '{ variant }';
|
|
649
|
+
return `// Suggested shared component
|
|
650
|
+
interface ${baseName}Props {
|
|
651
|
+
${sharedProps.map(p => `${p}: unknown;`).join('\n ')}
|
|
652
|
+
variant: '${comp1.name.toLowerCase()}' | '${comp2.name.toLowerCase()}';
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const ${baseName} = (${propsStr}: ${baseName}Props) => {
|
|
656
|
+
// Shared implementation with variant-specific rendering
|
|
657
|
+
};`;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Generate example code for shared hook suggestion
|
|
661
|
+
*/
|
|
662
|
+
function generateSharedHookExample(comp1, comp2) {
|
|
663
|
+
const sharedState = comp1.stateVariables
|
|
664
|
+
.filter(s1 => comp2.stateVariables.some(s2 => s2.stateType === s1.stateType));
|
|
665
|
+
const hookName = `use${findCommonBaseName(comp1.name, comp2.name)}Logic`;
|
|
666
|
+
return `// Suggested shared hook
|
|
667
|
+
const ${hookName} = () => {
|
|
668
|
+
${sharedState.map(s => `const [${s.name}, ${s.setter}] = ${s.stateType}(${s.initialValue || ''});`).join('\n ')}
|
|
669
|
+
|
|
670
|
+
// Shared logic here
|
|
671
|
+
|
|
672
|
+
return { ${sharedState.map(s => s.name).join(', ')} };
|
|
673
|
+
};`;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Find common base name between two component names
|
|
677
|
+
*/
|
|
678
|
+
function findCommonBaseName(name1, name2) {
|
|
679
|
+
// Try to find common prefix
|
|
680
|
+
let commonPrefix = '';
|
|
681
|
+
const minLen = Math.min(name1.length, name2.length);
|
|
682
|
+
for (let i = 0; i < minLen; i++) {
|
|
683
|
+
if (name1[i] === name2[i]) {
|
|
684
|
+
commonPrefix += name1[i];
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (commonPrefix.length >= 3) {
|
|
691
|
+
return commonPrefix;
|
|
692
|
+
}
|
|
693
|
+
// Try to find common suffix
|
|
694
|
+
let commonSuffix = '';
|
|
695
|
+
for (let i = 0; i < minLen; i++) {
|
|
696
|
+
if (name1[name1.length - 1 - i] === name2[name2.length - 1 - i]) {
|
|
697
|
+
commonSuffix = name1[name1.length - 1 - i] + commonSuffix;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (commonSuffix.length >= 3) {
|
|
704
|
+
return commonSuffix;
|
|
705
|
+
}
|
|
706
|
+
// Default to generic name
|
|
707
|
+
return 'Shared';
|
|
708
|
+
}
|
|
709
|
+
// ============================================================================
|
|
710
|
+
// Helper Functions - Analysis
|
|
711
|
+
// ============================================================================
|
|
712
|
+
/**
|
|
713
|
+
* Compare two components and return near-duplicate info if similar enough
|
|
714
|
+
*/
|
|
715
|
+
export function compareComponentsSemanticly(comp1, comp2, config = DEFAULT_NEAR_DUPLICATE_CONFIG) {
|
|
716
|
+
const breakdown = calculateSemanticSimilarity(comp1, comp2);
|
|
717
|
+
const similarity = calculateOverallSimilarity(breakdown, config.weights);
|
|
718
|
+
// Check if similarity meets threshold
|
|
719
|
+
if (similarity < config.similarityThreshold) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
// Check minimum feature similarities
|
|
723
|
+
if (breakdown.props < config.minPropsSimilarity && breakdown.jsxStructure < config.minJsxSimilarity) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const abstractionType = determineAbstractionType(comp1, comp2, breakdown);
|
|
727
|
+
const suggestions = generateAbstractionSuggestions(comp1, comp2, breakdown, abstractionType);
|
|
728
|
+
return {
|
|
729
|
+
component1: comp1,
|
|
730
|
+
component2: comp2,
|
|
731
|
+
similarity,
|
|
732
|
+
similarityBreakdown: breakdown,
|
|
733
|
+
suggestedAbstraction: abstractionType,
|
|
734
|
+
suggestions,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Analyze components for near-duplicates and abstraction opportunities
|
|
739
|
+
*/
|
|
740
|
+
export function analyzeNearDuplicates(components, config = DEFAULT_NEAR_DUPLICATE_CONFIG) {
|
|
741
|
+
const pairs = [];
|
|
742
|
+
const componentWithOpportunities = new Set();
|
|
743
|
+
// Compare all pairs of components
|
|
744
|
+
for (let i = 0; i < components.length; i++) {
|
|
745
|
+
for (let j = i + 1; j < components.length; j++) {
|
|
746
|
+
const comp1 = components[i];
|
|
747
|
+
const comp2 = components[j];
|
|
748
|
+
if (!comp1 || !comp2)
|
|
749
|
+
continue;
|
|
750
|
+
const pair = compareComponentsSemanticly(comp1, comp2, config);
|
|
751
|
+
if (pair) {
|
|
752
|
+
pairs.push(pair);
|
|
753
|
+
componentWithOpportunities.add(`${comp1.filePath}:${comp1.line}`);
|
|
754
|
+
componentWithOpportunities.add(`${comp2.filePath}:${comp2.line}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// Group by abstraction type
|
|
759
|
+
const abstractionGroups = buildAbstractionGroups(pairs, components);
|
|
760
|
+
return {
|
|
761
|
+
pairs,
|
|
762
|
+
abstractionGroups,
|
|
763
|
+
totalComponents: components.length,
|
|
764
|
+
componentsWithOpportunities: componentWithOpportunities.size,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Build abstraction groups from pairs
|
|
769
|
+
*/
|
|
770
|
+
function buildAbstractionGroups(pairs, allComponents) {
|
|
771
|
+
const groups = new Map();
|
|
772
|
+
const componentMap = new Map();
|
|
773
|
+
// Build component map
|
|
774
|
+
for (const comp of allComponents) {
|
|
775
|
+
componentMap.set(`${comp.filePath}:${comp.line}`, comp);
|
|
776
|
+
}
|
|
777
|
+
// Group components by abstraction type
|
|
778
|
+
for (const pair of pairs) {
|
|
779
|
+
const type = pair.suggestedAbstraction;
|
|
780
|
+
if (!groups.has(type)) {
|
|
781
|
+
groups.set(type, new Set());
|
|
782
|
+
}
|
|
783
|
+
const group = groups.get(type);
|
|
784
|
+
group.add(`${pair.component1.filePath}:${pair.component1.line}`);
|
|
785
|
+
group.add(`${pair.component2.filePath}:${pair.component2.line}`);
|
|
786
|
+
}
|
|
787
|
+
// Convert to AbstractionGroup array
|
|
788
|
+
const result = [];
|
|
789
|
+
for (const [type, componentKeys] of groups) {
|
|
790
|
+
const components = [];
|
|
791
|
+
for (const key of componentKeys) {
|
|
792
|
+
const comp = componentMap.get(key);
|
|
793
|
+
if (comp) {
|
|
794
|
+
components.push(comp);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
// Find shared features across all components in group
|
|
798
|
+
const sharedFeatures = findSharedFeatures(components);
|
|
799
|
+
// Calculate average confidence from pairs
|
|
800
|
+
const relevantPairs = pairs.filter(p => p.suggestedAbstraction === type);
|
|
801
|
+
const avgConfidence = relevantPairs.length > 0
|
|
802
|
+
? relevantPairs.reduce((sum, p) => sum + p.similarity, 0) / relevantPairs.length
|
|
803
|
+
: 0;
|
|
804
|
+
result.push({
|
|
805
|
+
abstractionType: type,
|
|
806
|
+
components,
|
|
807
|
+
sharedFeatures,
|
|
808
|
+
confidence: avgConfidence,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
return result.sort((a, b) => b.confidence - a.confidence);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Find features shared across all components
|
|
815
|
+
*/
|
|
816
|
+
function findSharedFeatures(components) {
|
|
817
|
+
if (components.length === 0)
|
|
818
|
+
return [];
|
|
819
|
+
const firstComp = components[0];
|
|
820
|
+
if (!firstComp)
|
|
821
|
+
return [];
|
|
822
|
+
const sharedProps = firstComp.props
|
|
823
|
+
.filter(p => components.every(c => c.props.some(cp => cp.name === p.name)))
|
|
824
|
+
.map(p => `prop:${p.name}`);
|
|
825
|
+
const sharedHooks = firstComp.hooks
|
|
826
|
+
.filter(h => components.every(c => c.hooks.some(ch => ch.name === h.name)))
|
|
827
|
+
.map(h => `hook:${h.name}`);
|
|
828
|
+
const sharedElements = firstComp.jsxElements
|
|
829
|
+
.filter(e => components.every(c => c.jsxElements.some(ce => ce.tagName === e.tagName)))
|
|
830
|
+
.map(e => `element:${e.tagName}`);
|
|
831
|
+
return [...sharedProps, ...sharedHooks, ...sharedElements];
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Generate a refactoring suggestion message
|
|
835
|
+
*/
|
|
836
|
+
export function generateRefactoringSuggestionMessage(pair) {
|
|
837
|
+
const { component1, component2, similarity, suggestedAbstraction } = pair;
|
|
838
|
+
const percentSimilar = Math.round(similarity * 100);
|
|
839
|
+
const abstractionDescriptions = {
|
|
840
|
+
'shared-component': 'creating a shared component with variant props',
|
|
841
|
+
'shared-hook': 'extracting shared logic into a custom hook',
|
|
842
|
+
'higher-order': 'using a Higher-Order Component (HOC)',
|
|
843
|
+
'render-props': 'using the render props pattern',
|
|
844
|
+
'composition': 'using component composition',
|
|
845
|
+
'utility-function': 'extracting utility functions',
|
|
846
|
+
};
|
|
847
|
+
return `Components '${component1.name}' and '${component2.name}' are ${percentSimilar}% semantically similar. ` +
|
|
848
|
+
`Consider ${abstractionDescriptions[suggestedAbstraction]}.`;
|
|
849
|
+
}
|
|
850
|
+
// ============================================================================
|
|
851
|
+
// Near Duplicate Detector Class
|
|
852
|
+
// ============================================================================
|
|
853
|
+
/**
|
|
854
|
+
* Detector for semantically similar components that could be abstracted
|
|
855
|
+
*
|
|
856
|
+
* Unlike DuplicateDetector which focuses on AST structure, this detector
|
|
857
|
+
* analyzes semantic/functional similarity including:
|
|
858
|
+
* - Similar prop patterns
|
|
859
|
+
* - Similar render patterns
|
|
860
|
+
* - Similar state management
|
|
861
|
+
* - Opportunities for shared hooks, HOCs, or render props
|
|
862
|
+
*
|
|
863
|
+
* @requirements 8.4 - THE Component_Detector SHALL detect near-duplicate components that should be abstracted
|
|
864
|
+
*/
|
|
865
|
+
export class NearDuplicateDetector extends ASTDetector {
|
|
866
|
+
id = 'components/near-duplicate';
|
|
867
|
+
category = 'components';
|
|
868
|
+
subcategory = 'abstraction-candidates';
|
|
869
|
+
name = 'Near Duplicate Detector';
|
|
870
|
+
description = 'Detects semantically similar components that could be refactored into shared abstractions';
|
|
871
|
+
supportedLanguages = ['typescript', 'javascript'];
|
|
872
|
+
config;
|
|
873
|
+
constructor(config = {}) {
|
|
874
|
+
super();
|
|
875
|
+
this.config = { ...DEFAULT_NEAR_DUPLICATE_CONFIG, ...config };
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Detect near-duplicate components in the project
|
|
879
|
+
*/
|
|
880
|
+
async detect(context) {
|
|
881
|
+
const patterns = [];
|
|
882
|
+
const violations = [];
|
|
883
|
+
// Extract semantic features from all components
|
|
884
|
+
const allComponents = this.extractAllComponents(context);
|
|
885
|
+
if (allComponents.length < 2) {
|
|
886
|
+
return this.createEmptyResult();
|
|
887
|
+
}
|
|
888
|
+
// Analyze for near-duplicates
|
|
889
|
+
const analysis = analyzeNearDuplicates(allComponents, this.config);
|
|
890
|
+
// Create pattern matches for abstraction opportunities
|
|
891
|
+
for (const group of analysis.abstractionGroups) {
|
|
892
|
+
if (group.confidence > 0.5) {
|
|
893
|
+
patterns.push({
|
|
894
|
+
patternId: `abstraction-opportunity-${group.abstractionType}`,
|
|
895
|
+
location: { file: context.file, line: 1, column: 1 },
|
|
896
|
+
confidence: group.confidence,
|
|
897
|
+
isOutlier: false,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// Create violations for near-duplicates involving the current file
|
|
902
|
+
for (const pair of analysis.pairs) {
|
|
903
|
+
if (this.involvesCurrentFile(pair, context.file)) {
|
|
904
|
+
const violation = this.createNearDuplicateViolation(pair, context.file);
|
|
905
|
+
violations.push(violation);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const confidence = analysis.totalComponents > 0
|
|
909
|
+
? 1 - (analysis.componentsWithOpportunities / analysis.totalComponents)
|
|
910
|
+
: 1.0;
|
|
911
|
+
return this.createResult(patterns, violations, confidence);
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Generate a quick fix for near-duplicate violations
|
|
915
|
+
*/
|
|
916
|
+
generateQuickFix(violation) {
|
|
917
|
+
// Extract component names from the violation message
|
|
918
|
+
const match = violation.message.match(/Components '([^']+)' and '([^']+)'/);
|
|
919
|
+
if (!match || !match[1] || !match[2]) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
const comp1Name = match[1];
|
|
923
|
+
const comp2Name = match[2];
|
|
924
|
+
const baseName = findCommonBaseName(comp1Name, comp2Name);
|
|
925
|
+
// Determine abstraction type from message
|
|
926
|
+
let abstractionType = 'shared-component';
|
|
927
|
+
if (violation.message.includes('custom hook')) {
|
|
928
|
+
abstractionType = 'shared-hook';
|
|
929
|
+
}
|
|
930
|
+
else if (violation.message.includes('HOC')) {
|
|
931
|
+
abstractionType = 'higher-order';
|
|
932
|
+
}
|
|
933
|
+
return {
|
|
934
|
+
title: `Extract shared ${abstractionType === 'shared-hook' ? 'hook' : 'component'}: ${baseName}`,
|
|
935
|
+
kind: 'refactor',
|
|
936
|
+
edit: {
|
|
937
|
+
changes: {},
|
|
938
|
+
documentChanges: [],
|
|
939
|
+
},
|
|
940
|
+
isPreferred: true,
|
|
941
|
+
confidence: 0.7,
|
|
942
|
+
preview: `Create a shared ${abstractionType} to reduce duplication between ${comp1Name} and ${comp2Name}`,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Extract all components from project files
|
|
947
|
+
*/
|
|
948
|
+
extractAllComponents(context) {
|
|
949
|
+
const components = [];
|
|
950
|
+
// Extract from current file using AST if available
|
|
951
|
+
if (context.ast) {
|
|
952
|
+
const fileComponents = this.extractComponentsFromAST(context.ast, context.file, context.content);
|
|
953
|
+
components.push(...fileComponents);
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
// Fallback to regex-based extraction
|
|
957
|
+
const fileComponents = this.extractComponentsFromContent(context.file, context.content);
|
|
958
|
+
components.push(...fileComponents);
|
|
959
|
+
}
|
|
960
|
+
return components;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Extract components from an AST
|
|
964
|
+
*/
|
|
965
|
+
extractComponentsFromAST(ast, filePath, content) {
|
|
966
|
+
const components = [];
|
|
967
|
+
const functionNodes = this.findNodesByTypes(ast, [
|
|
968
|
+
'function_declaration',
|
|
969
|
+
'arrow_function',
|
|
970
|
+
'function_expression',
|
|
971
|
+
]);
|
|
972
|
+
for (const node of functionNodes) {
|
|
973
|
+
if (isReactComponentNode(node, content)) {
|
|
974
|
+
const features = extractSemanticFeatures(node, filePath, content);
|
|
975
|
+
if (features) {
|
|
976
|
+
components.push(features);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return components;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Extract components from content using regex (fallback)
|
|
984
|
+
*/
|
|
985
|
+
extractComponentsFromContent(filePath, content) {
|
|
986
|
+
const components = [];
|
|
987
|
+
// Match component patterns
|
|
988
|
+
const patterns = [
|
|
989
|
+
// Arrow function: const Button = ({ ... }) => ...
|
|
990
|
+
/(?:export\s+)?const\s+([A-Z][a-zA-Z0-9]*)\s*(?::\s*(?:React\.)?(?:FC|FunctionComponent)\s*<[^>]*>\s*)?=\s*\([^)]*\)\s*=>\s*[\s\S]*?(?=\n(?:export\s+)?(?:const|function|class)\s+[A-Z]|\n*$)/g,
|
|
991
|
+
// Function declaration: function Button({ ... }) { ... }
|
|
992
|
+
/(?:export\s+)?function\s+([A-Z][a-zA-Z0-9]*)\s*\([^)]*\)\s*\{[\s\S]*?\n\}/g,
|
|
993
|
+
];
|
|
994
|
+
for (const pattern of patterns) {
|
|
995
|
+
let match;
|
|
996
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
997
|
+
const name = match[1];
|
|
998
|
+
if (!name)
|
|
999
|
+
continue;
|
|
1000
|
+
const sourceCode = match[0];
|
|
1001
|
+
const beforeMatch = content.slice(0, match.index);
|
|
1002
|
+
const line = beforeMatch.split('\n').length;
|
|
1003
|
+
// Create a mock AST node for feature extraction
|
|
1004
|
+
const mockNode = {
|
|
1005
|
+
type: 'function_declaration',
|
|
1006
|
+
text: sourceCode,
|
|
1007
|
+
startPosition: { row: line - 1, column: 0 },
|
|
1008
|
+
endPosition: { row: line - 1 + sourceCode.split('\n').length, column: 0 },
|
|
1009
|
+
children: [],
|
|
1010
|
+
};
|
|
1011
|
+
const features = extractSemanticFeatures(mockNode, filePath, content);
|
|
1012
|
+
if (features) {
|
|
1013
|
+
components.push(features);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return components;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Check if a near-duplicate pair involves the current file
|
|
1021
|
+
*/
|
|
1022
|
+
involvesCurrentFile(pair, currentFile) {
|
|
1023
|
+
return pair.component1.filePath === currentFile ||
|
|
1024
|
+
pair.component2.filePath === currentFile;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Create a violation for a near-duplicate pair
|
|
1028
|
+
*/
|
|
1029
|
+
createNearDuplicateViolation(pair, currentFile) {
|
|
1030
|
+
const { component1, component2, similarity, suggestedAbstraction } = pair;
|
|
1031
|
+
// Determine which component is in the current file
|
|
1032
|
+
const currentComponent = component1.filePath === currentFile ? component1 : component2;
|
|
1033
|
+
const otherComponent = component1.filePath === currentFile ? component2 : component1;
|
|
1034
|
+
const message = generateRefactoringSuggestionMessage(pair);
|
|
1035
|
+
const range = {
|
|
1036
|
+
start: { line: currentComponent.line, character: currentComponent.column },
|
|
1037
|
+
end: { line: currentComponent.line, character: currentComponent.column + currentComponent.name.length },
|
|
1038
|
+
};
|
|
1039
|
+
const abstractionDescriptions = {
|
|
1040
|
+
'shared-component': 'shared component with variant props',
|
|
1041
|
+
'shared-hook': 'custom hook for shared logic',
|
|
1042
|
+
'higher-order': 'Higher-Order Component (HOC)',
|
|
1043
|
+
'render-props': 'render props pattern',
|
|
1044
|
+
'composition': 'component composition',
|
|
1045
|
+
'utility-function': 'utility functions',
|
|
1046
|
+
};
|
|
1047
|
+
const quickFix = this.generateQuickFix({
|
|
1048
|
+
id: '',
|
|
1049
|
+
patternId: '',
|
|
1050
|
+
severity: 'info',
|
|
1051
|
+
file: currentFile,
|
|
1052
|
+
range,
|
|
1053
|
+
message,
|
|
1054
|
+
expected: '',
|
|
1055
|
+
actual: '',
|
|
1056
|
+
aiExplainAvailable: true,
|
|
1057
|
+
aiFixAvailable: true,
|
|
1058
|
+
firstSeen: new Date(),
|
|
1059
|
+
occurrences: 1,
|
|
1060
|
+
});
|
|
1061
|
+
const violation = {
|
|
1062
|
+
id: `near-duplicate-${currentComponent.name}-${otherComponent.name}-${currentFile.replace(/[^a-zA-Z0-9]/g, '-')}`,
|
|
1063
|
+
patternId: 'components/near-duplicate',
|
|
1064
|
+
severity: 'info',
|
|
1065
|
+
file: currentFile,
|
|
1066
|
+
range,
|
|
1067
|
+
message,
|
|
1068
|
+
expected: abstractionDescriptions[suggestedAbstraction],
|
|
1069
|
+
actual: `${Math.round(similarity * 100)}% semantic similarity`,
|
|
1070
|
+
aiExplainAvailable: true,
|
|
1071
|
+
aiFixAvailable: true,
|
|
1072
|
+
firstSeen: new Date(),
|
|
1073
|
+
occurrences: 1,
|
|
1074
|
+
};
|
|
1075
|
+
if (quickFix) {
|
|
1076
|
+
violation.quickFix = quickFix;
|
|
1077
|
+
}
|
|
1078
|
+
return violation;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
// ============================================================================
|
|
1082
|
+
// Factory Function
|
|
1083
|
+
// ============================================================================
|
|
1084
|
+
/**
|
|
1085
|
+
* Create a new NearDuplicateDetector instance
|
|
1086
|
+
*/
|
|
1087
|
+
export function createNearDuplicateDetector(config = {}) {
|
|
1088
|
+
return new NearDuplicateDetector(config);
|
|
1089
|
+
}
|
|
1090
|
+
//# sourceMappingURL=near-duplicate.js.map
|