srcly 0.1.6__tar.gz → 0.1.9__tar.gz
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.
- {srcly-0.1.6 → srcly-0.1.9}/PKG-INFO +5 -1
- {srcly-0.1.6 → srcly-0.1.9}/app/config.py +3 -2
- {srcly-0.1.6 → srcly-0.1.9}/app/models.py +6 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/routers/analysis.py +32 -10
- {srcly-0.1.6 → srcly-0.1.9}/app/run.py +35 -5
- srcly-0.1.9/app/services/analysis.py +331 -0
- srcly-0.1.9/app/services/cache.py +19 -0
- srcly-0.1.9/app/services/tree_sitter_analysis.py +187 -0
- srcly-0.1.9/app/static/assets/index-Bb-1BUOV.js +14 -0
- srcly-0.1.9/app/static/assets/index-D-JuyTZT.css +1 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/index.html +2 -2
- {srcly-0.1.6 → srcly-0.1.9}/pyproject.toml +5 -1
- srcly-0.1.6/app/services/analysis.py +0 -170
- srcly-0.1.6/app/services/cache.py +0 -28
- srcly-0.1.6/app/static/assets/index-CBBfoGZS.css +0 -1
- srcly-0.1.6/app/static/assets/index-DCyN-sgG.js +0 -18
- {srcly-0.1.6 → srcly-0.1.9}/.gitignore +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/README.md +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/.gitignore +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/__init__.py +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/main.py +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/routers/files.py +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/abap-BdImnpbu.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/actionscript-3-CfeIJUat.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/ada-bCR0ucgS.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/andromeeda-C-Jbm3Hp.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/angular-html-CU67Zn6k.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/angular-ts-BwZT4LLn.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/apache-Pmp26Uib.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/apex-DhZLUxFE.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/apl-dKokRX4l.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/applescript-Co6uUVPk.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/ara-BRHolxvo.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/asciidoc-Dv7Oe6Be.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/asm-D_Q5rh1f.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/astro-CbQHKStN.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/aurora-x-D-2ljcwZ.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/awk-DMzUqQB5.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/ayu-dark-Cv9koXgw.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/ballerina-BFfxhgS-.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/bat-BkioyH1T.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/beancount-k_qm7-4y.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/berry-D08WgyRC.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/bibtex-CHM0blh-.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/bicep-Bmn6On1c.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/blade-DVc8C-J4.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/bsl-BO_Y6i37.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/c-BIGW1oBm.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/cadence-Bv_4Rxtq.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/cairo-KRGpt6FW.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/catppuccin-frappe-DFWUc33u.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/catppuccin-latte-C9dUb6Cb.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/catppuccin-macchiato-DQyhUUbL.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/catppuccin-mocha-D87Tk5Gz.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/clarity-D53aC0YG.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/clojure-P80f7IUj.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/cmake-D1j8_8rp.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/cobol-nwyudZeR.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/codeowners-Bp6g37R7.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/codeql-DsOJ9woJ.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/coffee-Ch7k5sss.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/common-lisp-Cg-RD9OK.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/coq-DkFqJrB1.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/cpp-CofmeUqb.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/crystal-tKQVLTB8.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/csharp-CX12Zw3r.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/css-DPfMkruS.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/csv-fuZLfV_i.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/cue-D82EKSYY.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/cypher-COkxafJQ.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/d-85-TOEBH.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/dark-plus-eOWES_5F.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/dart-CF10PKvl.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/dax-CEL-wOlO.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/desktop-BmXAJ9_W.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/diff-D97Zzqfu.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/docker-BcOcwvcX.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/dotenv-Da5cRb03.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/dracula-BzJJZx-M.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/dracula-soft-BXkSAIEj.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/dream-maker-BtqSS_iP.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/edge-BkV0erSs.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/elixir-CDX3lj18.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/elm-DbKCFpqz.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/emacs-lisp-C9XAeP06.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/erb-BOJIQeun.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/erlang-DsQrWhSR.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/everforest-dark-BgDCqdQA.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/everforest-light-C8M2exoo.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/fennel-BYunw83y.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/fish-BvzEVeQv.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/fluent-C4IJs8-o.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/fortran-fixed-form-BZjJHVRy.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/fortran-free-form-D22FLkUw.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/fsharp-CXgrBDvD.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gdresource-B7Tvp0Sc.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gdscript-DTMYz4Jt.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gdshader-DkwncUOv.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/genie-D0YGMca9.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gherkin-DyxjwDmM.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/git-commit-F4YmCXRG.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/git-rebase-r7XF79zn.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/github-dark-DHJKELXO.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/github-dark-default-Cuk6v7N8.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/github-dark-dimmed-DH5Ifo-i.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/github-dark-high-contrast-E3gJ1_iC.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/github-light-DAi9KRSo.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/github-light-default-D7oLnXFd.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/github-light-high-contrast-BfjtVDDH.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gleam-BspZqrRM.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/glimmer-js-Rg0-pVw9.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/glimmer-ts-U6CK756n.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/glsl-DplSGwfg.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gnuplot-DdkO51Og.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/go-Dn2_MT6a.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/graphql-ChdNCCLP.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/groovy-gcz8RCvz.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gruvbox-dark-hard-CFHQjOhq.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gruvbox-dark-medium-GsRaNv29.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gruvbox-dark-soft-CVdnzihN.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gruvbox-light-hard-CH1njM8p.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gruvbox-light-medium-DRw_LuNl.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/gruvbox-light-soft-hJgmCMqR.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/hack-CaT9iCJl.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/haml-B8DHNrY2.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/handlebars-BL8al0AC.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/haskell-Df6bDoY_.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/haxe-CzTSHFRz.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/hcl-BWvSN4gD.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/hjson-D5-asLiD.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/hlsl-D3lLCCz7.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/houston-DnULxvSX.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/html-GMplVEZG.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/html-derivative-BFtXZ54Q.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/http-jrhK8wxY.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/hurl-irOxFIW8.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/hxml-Bvhsp5Yf.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/hy-DFXneXwc.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/imba-DGztddWO.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/ini-BEwlwnbL.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/java-CylS5w8V.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/javascript-wDzz0qaB.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/jinja-4LBKfQ-Z.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/jison-wvAkD_A8.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/json-Cp-IABpG.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/json5-C9tS-k6U.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/jsonc-Des-eS-w.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/jsonl-DcaNXYhu.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/jsonnet-DFQXde-d.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/jssm-C2t-YnRu.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/jsx-g9-lgVsj.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/julia-C8NyazO9.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/kanagawa-dragon-CkXjmgJE.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/kanagawa-lotus-CfQXZHmo.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/kanagawa-wave-DWedfzmr.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/kdl-DV7GczEv.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/kotlin-BdnUsdx6.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/kusto-BvAqAH-y.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/laserwave-DUszq2jm.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/latex-BUKiar2Z.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/lean-DP1Csr6i.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/less-B1dDrJ26.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/light-plus-B7mTdjB0.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/liquid-DYVedYrR.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/llvm-BtvRca6l.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/log-2UxHyX5q.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/logo-BtOb2qkB.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/lua-BbnMAYS6.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/luau-CXu1NL6O.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/make-CHLpvVh8.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/markdown-Cvjx9yec.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/marko-CPi9NSCl.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/material-theme-D5KoaKCx.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/material-theme-darker-BfHTSMKl.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/material-theme-lighter-B0m2ddpp.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/material-theme-ocean-CyktbL80.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/material-theme-palenight-Csfq5Kiy.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/matlab-D7o27uSR.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/mdc-DUICxH0z.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/mdx-Cmh6b_Ma.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/mermaid-DKYwYmdq.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/min-dark-CafNBF8u.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/min-light-CTRr51gU.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/mipsasm-CKIfxQSi.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/mojo-1DNp92w6.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/monokai-D4h5O-jR.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/move-Bu9oaDYs.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/narrat-DRg8JJMk.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/nextflow-CUEJCptM.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/nginx-DknmC5AR.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/night-owl-C39BiMTA.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/nim-CVrawwO9.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/nix-BbRYJGeE.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/nord-Ddv68eIx.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/nushell-C-sUppwS.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/objective-c-DXmwc3jG.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/objective-cpp-CLxacb5B.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/ocaml-C0hk2d4L.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/one-dark-pro-DVMEJ2y_.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/one-light-PoHY5YXO.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/pascal-D93ZcfNL.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/perl-C0TMdlhV.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/php-CDn_0X-4.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/pkl-u5AG7uiY.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/plastic-3e1v2bzS.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/plsql-ChMvpjG-.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/po-BTJTHyun.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/poimandres-CS3Unz2-.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/polar-C0HS_06l.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/postcss-CXtECtnM.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/powerquery-CEu0bR-o.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/powershell-Dpen1YoG.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/prisma-Dd19v3D-.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/prolog-CbFg5uaA.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/proto-DyJlTyXw.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/pug-CGlum2m_.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/puppet-BMWR74SV.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/purescript-CklMAg4u.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/python-B6aJPvgy.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/qml-3beO22l8.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/qmldir-C8lEn-DE.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/qss-IeuSbFQv.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/r-DiinP2Uv.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/racket-BqYA7rlc.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/raku-DXvB9xmW.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/razor-WgofotgN.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/red-bN70gL4F.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/reg-C-SQnVFl.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/regexp-CDVJQ6XC.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/rel-C3B-1QV4.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/riscv-BM1_JUlF.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/rose-pine-BHrmToEH.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/rose-pine-dawn-CnK8MTSM.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/rose-pine-moon-NleAzG8P.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/rosmsg-BJDFO7_C.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/rst-B0xPkSld.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/ruby-BvKwtOVI.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/rust-B1yitclQ.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/sas-cz2c8ADy.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/sass-Cj5Yp3dK.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/scala-C151Ov-r.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/scheme-C98Dy4si.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/scss-OYdSNvt2.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/sdbl-DVxCFoDh.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/shaderlab-Dg9Lc6iA.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/shellscript-Yzrsuije.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/shellsession-BADoaaVG.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/slack-dark-BthQWCQV.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/slack-ochin-DqwNpetd.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/smalltalk-BERRCDM3.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/snazzy-light-Bw305WKR.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/solarized-dark-DXbdFlpD.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/solarized-light-L9t79GZl.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/solidity-BbcW6ACK.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/soy-Brmx7dQM.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/sparql-rVzFXLq3.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/splunk-BtCnVYZw.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/sql-BLtJtn59.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/ssh-config-_ykCGR6B.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/stata-BH5u7GGu.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/stylus-BEDo0Tqx.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/svelte-3Dk4HxPD.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/swift-Dg5xB15N.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/synthwave-84-CbfX1IO0.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/system-verilog-CnnmHF94.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/systemd-4A_iFExJ.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/talonscript-CkByrt1z.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/tasl-QIJgUcNo.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/tcl-dwOrl1Do.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/templ-W15q3VgB.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/terraform-BETggiCN.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/tex-Cppo0RY3.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/tokyo-night-hegEt444.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/toml-vGWfd6FD.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/ts-tags-zn1MmPIZ.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/tsv-B_m7g4N7.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/tsx-COt5Ahok.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/turtle-BsS91CYL.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/twig-CO9l9SDP.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/typescript-BPQ3VLAy.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/typespec-Df68jz8_.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/typst-DHCkPAjA.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/v-BcVCzyr7.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vala-CsfeWuGM.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vb-D17OF-Vu.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/verilog-BQ8w6xss.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vesper-DU1UobuO.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vhdl-CeAyd5Ju.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/viml-CJc9bBzg.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vitesse-black-Bkuqu6BP.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vitesse-dark-D0r3Knsf.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vitesse-light-CVO1_9PV.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vue-CCoi5OLL.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vue-html-DAAvJJDi.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vue-vine-_Ih-lPRR.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/vyper-CDx5xZoG.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/wasm-CG6Dc4jp.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/wasm-MzD3tlZU.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/wenyan-BV7otONQ.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/wgsl-Dx-B1_4e.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/wikitext-BhOHFoWU.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/wit-5i3qLPDT.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/wolfram-lXgVvXCa.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/xml-sdJ4AIDG.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/xsl-CtQFsRM5.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/yaml-Buea-lGh.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/zenscript-DVFEvuxE.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/assets/zig-VOosw3JB.js +0 -0
- {srcly-0.1.6 → srcly-0.1.9}/app/static/vite.svg +0 -0
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: srcly
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Interactive codebase treemap and metrics viewer.
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Requires-Dist: fastapi>=0.121.3
|
|
7
7
|
Requires-Dist: lizard>=1.19.0
|
|
8
|
+
Requires-Dist: pathspec>=0.12.1
|
|
8
9
|
Requires-Dist: pydantic>=2.12.4
|
|
10
|
+
Requires-Dist: pytest>=9.0.1
|
|
11
|
+
Requires-Dist: tree-sitter-typescript>=0.23.2
|
|
12
|
+
Requires-Dist: tree-sitter>=0.25.2
|
|
9
13
|
Requires-Dist: uvicorn>=0.38.0
|
|
10
14
|
Description-Content-Type: text/markdown
|
|
11
15
|
|
|
@@ -25,7 +25,8 @@ IGNORE_FILES: Set[str] = {
|
|
|
25
25
|
|
|
26
26
|
IGNORE_EXTENSIONS: Set[str] = {
|
|
27
27
|
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.pdf',
|
|
28
|
-
'.zip', '.tar', '.gz', '.map', '.min.js', '.min.css', '.json', '.xml', '.txt',
|
|
28
|
+
'.zip', '.tar', '.gz', '.map', '.min.js', '.min.css', '.json', '.xml', '.txt',
|
|
29
29
|
'.ttf', '.woff', '.woff2', '.eot', '.otf', '.mp4', '.mp3', '.wav', '.webm', '.ogg',
|
|
30
|
-
'.exe', '.dll', '.so', '.dylib', '.bin', '.iso', '.dmg', '.pkg', '.deb', '.rpm'
|
|
30
|
+
'.exe', '.dll', '.so', '.dylib', '.bin', '.iso', '.dmg', '.pkg', '.deb', '.rpm',
|
|
31
|
+
'.csv', '.lock'
|
|
31
32
|
}
|
|
@@ -7,6 +7,10 @@ class Metrics(BaseModel):
|
|
|
7
7
|
# so we store it as a float to match the analysis values.
|
|
8
8
|
complexity: float = 0.0
|
|
9
9
|
function_count: int = 0
|
|
10
|
+
last_modified: float = 0.0
|
|
11
|
+
gitignored_count: int = 0
|
|
12
|
+
file_size: int = 0
|
|
13
|
+
file_count: int = 0
|
|
10
14
|
|
|
11
15
|
class Node(BaseModel):
|
|
12
16
|
name: str
|
|
@@ -15,6 +19,8 @@ class Node(BaseModel):
|
|
|
15
19
|
metrics: Metrics
|
|
16
20
|
# Use default_factory to avoid sharing the same list across instances
|
|
17
21
|
children: List["Node"] = Field(default_factory=list)
|
|
22
|
+
start_line: int = 0
|
|
23
|
+
end_line: int = 0
|
|
18
24
|
|
|
19
25
|
model_config = {
|
|
20
26
|
"populate_by_name": True
|
|
@@ -44,20 +44,18 @@ async def refresh_analysis():
|
|
|
44
44
|
return tree
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
async def get_analysis_context():
|
|
47
|
+
def _estimate_counts(root: Path) -> tuple[int, int]:
|
|
49
48
|
"""
|
|
50
|
-
Return
|
|
49
|
+
Return an estimated (file_count, folder_count) for a given root directory.
|
|
51
50
|
|
|
52
|
-
This
|
|
53
|
-
|
|
51
|
+
This applies the same ignore rules used during a full analysis to keep the
|
|
52
|
+
estimate reasonably fast while still informative.
|
|
54
53
|
"""
|
|
55
|
-
root = ROOT_PATH
|
|
56
54
|
file_count = 0
|
|
57
55
|
folder_count = 0
|
|
58
56
|
|
|
59
57
|
try:
|
|
60
|
-
for
|
|
58
|
+
for _, dirnames, filenames in os.walk(root):
|
|
61
59
|
# Apply ignore rules for directories to keep the estimate reasonable
|
|
62
60
|
dirnames[:] = [
|
|
63
61
|
d for d in dirnames
|
|
@@ -70,8 +68,32 @@ async def get_analysis_context():
|
|
|
70
68
|
file_count = 0
|
|
71
69
|
folder_count = 0
|
|
72
70
|
|
|
71
|
+
return file_count, folder_count
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get("/context")
|
|
75
|
+
async def get_analysis_context():
|
|
76
|
+
"""
|
|
77
|
+
Return basic information about the current analysis root directory.
|
|
78
|
+
|
|
79
|
+
This is used by the client to offer one-click analysis options on first
|
|
80
|
+
load, for both the current working directory and the repository root.
|
|
81
|
+
"""
|
|
82
|
+
current_root = ROOT_PATH
|
|
83
|
+
repo_root = current_root.parent
|
|
84
|
+
|
|
85
|
+
current_file_count, current_folder_count = _estimate_counts(current_root)
|
|
86
|
+
|
|
87
|
+
repo_file_count = 0
|
|
88
|
+
repo_folder_count = 0
|
|
89
|
+
if repo_root.exists():
|
|
90
|
+
repo_file_count, repo_folder_count = _estimate_counts(repo_root)
|
|
91
|
+
|
|
73
92
|
return {
|
|
74
|
-
"root_path": str(
|
|
75
|
-
"file_count":
|
|
76
|
-
"folder_count":
|
|
93
|
+
"root_path": str(current_root),
|
|
94
|
+
"file_count": current_file_count,
|
|
95
|
+
"folder_count": current_folder_count,
|
|
96
|
+
"repo_root_path": str(repo_root),
|
|
97
|
+
"repo_file_count": repo_file_count,
|
|
98
|
+
"repo_folder_count": repo_folder_count,
|
|
77
99
|
}
|
|
@@ -26,11 +26,34 @@ def _open_browser_later(url: str, delay: float = 1.0) -> None:
|
|
|
26
26
|
thread.start()
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _find_repo_root(start_path: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Walk upwards from ``start_path`` to find a Git repository root.
|
|
32
|
+
|
|
33
|
+
Returns the first directory that contains a `.git` directory or file.
|
|
34
|
+
If none is found, the original ``start_path`` is returned.
|
|
35
|
+
"""
|
|
36
|
+
current = os.path.abspath(start_path)
|
|
37
|
+
|
|
38
|
+
while True:
|
|
39
|
+
git_path = os.path.join(current, ".git")
|
|
40
|
+
if os.path.exists(git_path):
|
|
41
|
+
return current
|
|
42
|
+
|
|
43
|
+
parent = os.path.dirname(current)
|
|
44
|
+
if parent == current:
|
|
45
|
+
# Reached filesystem root; fall back to the original start path.
|
|
46
|
+
return os.path.abspath(start_path)
|
|
47
|
+
|
|
48
|
+
current = parent
|
|
49
|
+
|
|
50
|
+
|
|
29
51
|
def main(argv: list[str] | None = None) -> None:
|
|
30
52
|
"""
|
|
31
53
|
Entry point for the CLI.
|
|
32
54
|
|
|
33
|
-
-
|
|
55
|
+
- With no path argument, uses the enclosing Git repo root as the codebase root.
|
|
56
|
+
- If a path is provided (including "."), uses that as the codebase root.
|
|
34
57
|
- Starts the FastAPI server.
|
|
35
58
|
- Opens the default browser to the app URL.
|
|
36
59
|
"""
|
|
@@ -38,14 +61,17 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
38
61
|
prog="srcly",
|
|
39
62
|
description=(
|
|
40
63
|
"Interactive codebase treemap and metrics viewer. "
|
|
41
|
-
"By default, analyzes the
|
|
64
|
+
"By default, analyzes the enclosing Git repository root."
|
|
42
65
|
),
|
|
43
66
|
)
|
|
44
67
|
parser.add_argument(
|
|
45
68
|
"path",
|
|
46
69
|
nargs="?",
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
help=(
|
|
71
|
+
"Path to the codebase to analyze. "
|
|
72
|
+
"If omitted, the enclosing Git repo root is used. "
|
|
73
|
+
'Use "." explicitly to analyze the current directory.'
|
|
74
|
+
),
|
|
49
75
|
)
|
|
50
76
|
parser.add_argument(
|
|
51
77
|
"--host",
|
|
@@ -61,7 +87,11 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
61
87
|
|
|
62
88
|
args = parser.parse_args(argv)
|
|
63
89
|
|
|
64
|
-
|
|
90
|
+
if args.path is None:
|
|
91
|
+
target_path = _find_repo_root(os.getcwd())
|
|
92
|
+
else:
|
|
93
|
+
target_path = os.path.abspath(args.path)
|
|
94
|
+
|
|
65
95
|
if not os.path.exists(target_path):
|
|
66
96
|
raise SystemExit(f"Path does not exist: {target_path}")
|
|
67
97
|
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import lizard
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from app.models import Node, Metrics
|
|
7
|
+
from app.config import IGNORE_DIRS, IGNORE_FILES, IGNORE_EXTENSIONS
|
|
8
|
+
from app.services.tree_sitter_analysis import TreeSitterAnalyzer
|
|
9
|
+
from pathspec import PathSpec
|
|
10
|
+
|
|
11
|
+
_ts_analyzer = None
|
|
12
|
+
|
|
13
|
+
def get_ts_analyzer():
|
|
14
|
+
global _ts_analyzer
|
|
15
|
+
if _ts_analyzer is None:
|
|
16
|
+
_ts_analyzer = TreeSitterAnalyzer()
|
|
17
|
+
return _ts_analyzer
|
|
18
|
+
|
|
19
|
+
def find_repo_root(start_path: Path) -> Path:
|
|
20
|
+
current = start_path.resolve()
|
|
21
|
+
for parent in [current, *current.parents]:
|
|
22
|
+
if (parent / ".git").exists(): return parent
|
|
23
|
+
return current
|
|
24
|
+
|
|
25
|
+
def create_node(name: str, node_type: str, path: str) -> Node:
|
|
26
|
+
return Node(
|
|
27
|
+
name=name,
|
|
28
|
+
type=node_type,
|
|
29
|
+
path=path,
|
|
30
|
+
metrics=Metrics(),
|
|
31
|
+
children=[]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def attach_file_metrics(node: Node, file_info) -> None:
|
|
35
|
+
total_loc = file_info.nloc
|
|
36
|
+
node.metrics.loc = total_loc
|
|
37
|
+
node.metrics.complexity = file_info.average_cyclomatic_complexity
|
|
38
|
+
node.metrics.function_count = len(file_info.function_list)
|
|
39
|
+
node.metrics.file_count = 1
|
|
40
|
+
|
|
41
|
+
# Set start/end line for the file (approximate, 1 to total lines)
|
|
42
|
+
# We don't strictly have this from lizard always, but we can infer or leave 0.
|
|
43
|
+
# For now, let's leave file start/end as 0 unless we want to read the file.
|
|
44
|
+
|
|
45
|
+
# Calculate sum of function LOCs
|
|
46
|
+
func_sum_loc = 0
|
|
47
|
+
|
|
48
|
+
def convert_function(func, parent_path: str) -> Node:
|
|
49
|
+
func_node = create_node(func.name, "function", f"{parent_path}::{func.name}")
|
|
50
|
+
func_node.metrics.loc = func.nloc
|
|
51
|
+
func_node.metrics.complexity = func.cyclomatic_complexity
|
|
52
|
+
|
|
53
|
+
if hasattr(func, 'start_line'):
|
|
54
|
+
func_node.start_line = func.start_line
|
|
55
|
+
if hasattr(func, 'end_line'):
|
|
56
|
+
func_node.end_line = func.end_line
|
|
57
|
+
|
|
58
|
+
# Process children if they exist (for TS/TSX)
|
|
59
|
+
if hasattr(func, 'children') and func.children:
|
|
60
|
+
for child in func.children:
|
|
61
|
+
child_node = convert_function(child, func_node.path)
|
|
62
|
+
func_node.children.append(child_node)
|
|
63
|
+
|
|
64
|
+
return func_node
|
|
65
|
+
|
|
66
|
+
for func in file_info.function_list:
|
|
67
|
+
func_node = convert_function(func, node.path)
|
|
68
|
+
node.children.append(func_node)
|
|
69
|
+
func_sum_loc += func.nloc
|
|
70
|
+
|
|
71
|
+
# CRITICAL FIX: Add a virtual node for the "Glue Code" (Imports, Exports, Global Vars)
|
|
72
|
+
remainder = total_loc - func_sum_loc
|
|
73
|
+
if remainder > 0:
|
|
74
|
+
misc_node = create_node("(misc/imports)", "code_fragment", f"{node.path}::__misc__")
|
|
75
|
+
misc_node.metrics.loc = remainder
|
|
76
|
+
misc_node.metrics.complexity = 0 # Glue code is usually simple
|
|
77
|
+
node.children.append(misc_node)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _translate_gitignore_pattern(raw_line: str, base_rel: str) -> str | None:
|
|
81
|
+
"""
|
|
82
|
+
Translate a single .gitignore pattern that lives in a directory `base_rel`
|
|
83
|
+
(relative to the repo root) into a repo-root-relative gitwildmatch pattern.
|
|
84
|
+
|
|
85
|
+
This approximates Git's semantics including:
|
|
86
|
+
- patterns starting with '!' (negation)
|
|
87
|
+
- patterns starting with '/' (anchored to the .gitignore directory)
|
|
88
|
+
- patterns without '/' applying within the directory subtree
|
|
89
|
+
"""
|
|
90
|
+
line = raw_line.rstrip("\n")
|
|
91
|
+
if not line or line.lstrip().startswith("#"):
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
negated = line.startswith("!")
|
|
95
|
+
body = line[1:] if negated else line
|
|
96
|
+
|
|
97
|
+
# Strip leading slash: anchored to the directory containing the .gitignore
|
|
98
|
+
if body.startswith("/"):
|
|
99
|
+
body = body[1:]
|
|
100
|
+
|
|
101
|
+
# Compute prefix for this .gitignore directory
|
|
102
|
+
prefix = f"{base_rel}/" if base_rel else ""
|
|
103
|
+
|
|
104
|
+
# If the pattern contains a slash, it's relative to the directory root.
|
|
105
|
+
# Otherwise, it should match that name anywhere under the directory.
|
|
106
|
+
if "/" in body:
|
|
107
|
+
pat = prefix + body
|
|
108
|
+
else:
|
|
109
|
+
if base_rel:
|
|
110
|
+
pat = f"{base_rel}/**/{body}"
|
|
111
|
+
else:
|
|
112
|
+
pat = f"**/{body}"
|
|
113
|
+
|
|
114
|
+
return f"!{pat}" if negated else pat
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _load_gitignore_spec(root_path: Path) -> tuple[Path, PathSpec | None]:
|
|
118
|
+
"""
|
|
119
|
+
Load a PathSpec representing .gitignore rules visible from the given
|
|
120
|
+
root path, honoring nested .gitignore files similarly to Git.
|
|
121
|
+
|
|
122
|
+
We treat the *repository root* (where .git lives) as the base for all
|
|
123
|
+
ignore patterns, so that scanning a subdirectory still respects repo-level
|
|
124
|
+
.gitignore files and nested ones.
|
|
125
|
+
"""
|
|
126
|
+
repo_root = find_repo_root(root_path)
|
|
127
|
+
|
|
128
|
+
all_patterns: list[str] = []
|
|
129
|
+
|
|
130
|
+
for dirpath, dirnames, filenames in os.walk(repo_root):
|
|
131
|
+
# Never look inside the .git directory for ignore rules
|
|
132
|
+
if ".git" in dirnames:
|
|
133
|
+
dirnames.remove(".git")
|
|
134
|
+
|
|
135
|
+
if ".gitignore" not in filenames:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
gitignore_file = Path(dirpath) / ".gitignore"
|
|
139
|
+
base_rel = (
|
|
140
|
+
str(Path(dirpath).relative_to(repo_root).as_posix())
|
|
141
|
+
if Path(dirpath) != repo_root
|
|
142
|
+
else ""
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
with open(gitignore_file, "r") as f:
|
|
146
|
+
for raw in f:
|
|
147
|
+
translated = _translate_gitignore_pattern(raw, base_rel)
|
|
148
|
+
if translated is not None:
|
|
149
|
+
all_patterns.append(translated)
|
|
150
|
+
|
|
151
|
+
if not all_patterns:
|
|
152
|
+
return repo_root, None
|
|
153
|
+
|
|
154
|
+
spec = PathSpec.from_lines("gitwildmatch", all_patterns)
|
|
155
|
+
return repo_root, spec
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _is_gitignored(path: Path, ignore_root: Path, spec: PathSpec | None) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
Return True if the given path should be ignored according to the
|
|
161
|
+
provided PathSpec and root_path.
|
|
162
|
+
"""
|
|
163
|
+
if spec is None:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
rel = path.relative_to(ignore_root)
|
|
168
|
+
except ValueError:
|
|
169
|
+
rel = path
|
|
170
|
+
|
|
171
|
+
rel_str = rel.as_posix()
|
|
172
|
+
return spec.match_file(rel_str)
|
|
173
|
+
|
|
174
|
+
def aggregate_metrics(node: Node) -> Metrics:
|
|
175
|
+
if not node.children: return node.metrics
|
|
176
|
+
|
|
177
|
+
total_loc = 0
|
|
178
|
+
max_complexity = 0
|
|
179
|
+
total_funcs = 0
|
|
180
|
+
|
|
181
|
+
for child in node.children:
|
|
182
|
+
child_metrics = aggregate_metrics(child)
|
|
183
|
+
total_loc += child_metrics.loc
|
|
184
|
+
max_complexity = max(max_complexity, child_metrics.complexity)
|
|
185
|
+
total_funcs += child_metrics.function_count
|
|
186
|
+
|
|
187
|
+
# For Folders: Sum of children
|
|
188
|
+
# For Files: We trust the attach_file_metrics logic (which includes __misc__)
|
|
189
|
+
if node.type == "folder":
|
|
190
|
+
node.metrics.loc = total_loc
|
|
191
|
+
node.metrics.complexity = max_complexity
|
|
192
|
+
node.metrics.function_count = total_funcs
|
|
193
|
+
|
|
194
|
+
# Aggregate last_modified (max of children) and gitignored_count (sum of children)
|
|
195
|
+
node.metrics.last_modified = max((child.metrics.last_modified for child in node.children), default=0.0)
|
|
196
|
+
node.metrics.gitignored_count = sum(child.metrics.gitignored_count for child in node.children)
|
|
197
|
+
node.metrics.file_size = sum(child.metrics.file_size for child in node.children)
|
|
198
|
+
node.metrics.file_count = sum(child.metrics.file_count for child in node.children)
|
|
199
|
+
|
|
200
|
+
return node.metrics
|
|
201
|
+
|
|
202
|
+
def analyze_single_file(file_path: str):
|
|
203
|
+
"""
|
|
204
|
+
Wrapper to analyze a single file safely.
|
|
205
|
+
Must be top-level for multiprocessing pickling.
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
if file_path.endswith('.ts') or file_path.endswith('.tsx'):
|
|
209
|
+
analyzer = get_ts_analyzer()
|
|
210
|
+
return analyzer.analyze_file(file_path)
|
|
211
|
+
else:
|
|
212
|
+
return lizard.analyze_file(file_path)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
# Return error info instead of crashing
|
|
215
|
+
return {"error": str(e), "filename": file_path}
|
|
216
|
+
|
|
217
|
+
def scan_codebase(root_path: Path) -> Node:
|
|
218
|
+
print(f"🔍 Scanning: {root_path}", flush=True)
|
|
219
|
+
|
|
220
|
+
# Load .gitignore spec (repo-wide, with nested .gitignore support)
|
|
221
|
+
ignore_root, gitignore_spec = _load_gitignore_spec(root_path)
|
|
222
|
+
|
|
223
|
+
files_to_scan: list[str] = []
|
|
224
|
+
ignored_counts: dict[str, int] = {}
|
|
225
|
+
|
|
226
|
+
for root_dir, dirs, files in os.walk(root_path):
|
|
227
|
+
root_dir_path = Path(root_dir)
|
|
228
|
+
|
|
229
|
+
# Apply ignore dirs from config and .gitignore
|
|
230
|
+
# We must modify dirs in-place to prune traversal
|
|
231
|
+
pruned_dirs: list[str] = []
|
|
232
|
+
for d in dirs:
|
|
233
|
+
if d in IGNORE_DIRS:
|
|
234
|
+
continue
|
|
235
|
+
dir_path = root_dir_path / d
|
|
236
|
+
if _is_gitignored(dir_path, ignore_root, gitignore_spec):
|
|
237
|
+
# Entire directory is ignored; we skip traversing into it.
|
|
238
|
+
continue
|
|
239
|
+
pruned_dirs.append(d)
|
|
240
|
+
dirs[:] = pruned_dirs
|
|
241
|
+
|
|
242
|
+
current_ignored_count = 0
|
|
243
|
+
for file in files:
|
|
244
|
+
if file in IGNORE_FILES:
|
|
245
|
+
continue
|
|
246
|
+
if Path(file).suffix in IGNORE_EXTENSIONS:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
file_path = root_dir_path / file
|
|
250
|
+
|
|
251
|
+
if _is_gitignored(file_path, ignore_root, gitignore_spec):
|
|
252
|
+
current_ignored_count += 1
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
files_to_scan.append(str(file_path))
|
|
256
|
+
|
|
257
|
+
if current_ignored_count > 0:
|
|
258
|
+
ignored_counts[str(root_dir_path)] = current_ignored_count
|
|
259
|
+
|
|
260
|
+
print(f"📂 Analyzing {len(files_to_scan)} source files...", flush=True)
|
|
261
|
+
|
|
262
|
+
analysis_results = []
|
|
263
|
+
|
|
264
|
+
# Use ProcessPoolExecutor for better control and per-file error handling
|
|
265
|
+
# lizard.analyze_files was opaque and crashing the whole pool on one error
|
|
266
|
+
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
|
|
267
|
+
future_to_file = {executor.submit(analyze_single_file, f): f for f in files_to_scan}
|
|
268
|
+
|
|
269
|
+
completed_count = 0
|
|
270
|
+
total_count = len(files_to_scan)
|
|
271
|
+
|
|
272
|
+
for future in concurrent.futures.as_completed(future_to_file):
|
|
273
|
+
file = future_to_file[future]
|
|
274
|
+
completed_count += 1
|
|
275
|
+
try:
|
|
276
|
+
# Add a timeout to prevent hanging on single files
|
|
277
|
+
result = future.result(timeout=5)
|
|
278
|
+
if isinstance(result, dict) and "error" in result:
|
|
279
|
+
print(f"❌ [{completed_count}/{total_count}] Error analyzing {file}: {result['error']}", flush=True)
|
|
280
|
+
else:
|
|
281
|
+
# Success
|
|
282
|
+
print(f"✅ [{completed_count}/{total_count}] Analyzed {file}", flush=True)
|
|
283
|
+
analysis_results.append(result)
|
|
284
|
+
except concurrent.futures.TimeoutError:
|
|
285
|
+
print(f"❌ [{completed_count}/{total_count}] Timeout analyzing {file} (skipped)", flush=True)
|
|
286
|
+
except Exception as exc:
|
|
287
|
+
print(f"❌ [{completed_count}/{total_count}] Exception analyzing {file}: {exc}", flush=True)
|
|
288
|
+
|
|
289
|
+
tree_root = create_node("root", "folder", str(root_path))
|
|
290
|
+
node_map = {str(root_path): tree_root}
|
|
291
|
+
|
|
292
|
+
for file_info in analysis_results:
|
|
293
|
+
path_obj = Path(file_info.filename)
|
|
294
|
+
try: rel_path = path_obj.relative_to(root_path)
|
|
295
|
+
except ValueError: continue
|
|
296
|
+
|
|
297
|
+
parts = rel_path.parts
|
|
298
|
+
current_node = tree_root
|
|
299
|
+
current_path = root_path
|
|
300
|
+
|
|
301
|
+
# Build Folder Tree
|
|
302
|
+
for part in parts[:-1]:
|
|
303
|
+
next_path = current_path / part
|
|
304
|
+
next_path_str = str(next_path)
|
|
305
|
+
if next_path_str not in node_map:
|
|
306
|
+
new_folder = create_node(part, "folder", next_path_str)
|
|
307
|
+
# Set gitignored count if we have it for this folder
|
|
308
|
+
if next_path_str in ignored_counts:
|
|
309
|
+
new_folder.metrics.gitignored_count = ignored_counts[next_path_str]
|
|
310
|
+
current_node.children.append(new_folder)
|
|
311
|
+
node_map[next_path_str] = new_folder
|
|
312
|
+
current_node = node_map[next_path_str]
|
|
313
|
+
current_path = next_path
|
|
314
|
+
|
|
315
|
+
# Add File
|
|
316
|
+
file_node = create_node(parts[-1], "file", str(path_obj))
|
|
317
|
+
file_node = create_node(parts[-1], "file", str(path_obj))
|
|
318
|
+
attach_file_metrics(file_node, file_info)
|
|
319
|
+
# Set last_modified
|
|
320
|
+
try:
|
|
321
|
+
stat = os.stat(path_obj)
|
|
322
|
+
file_node.metrics.last_modified = stat.st_mtime
|
|
323
|
+
file_node.metrics.file_size = stat.st_size
|
|
324
|
+
except OSError:
|
|
325
|
+
file_node.metrics.last_modified = 0.0
|
|
326
|
+
file_node.metrics.file_size = 0
|
|
327
|
+
|
|
328
|
+
current_node.children.append(file_node)
|
|
329
|
+
|
|
330
|
+
aggregate_metrics(tree_root)
|
|
331
|
+
return tree_root
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from app.models import Node
|
|
5
|
+
|
|
6
|
+
CACHE_FILE_NAME = "codebase_mri.json"
|
|
7
|
+
|
|
8
|
+
def get_cache_path(root_path: Path) -> Path:
|
|
9
|
+
return root_path / CACHE_FILE_NAME
|
|
10
|
+
|
|
11
|
+
def save_analysis(root_path: Path, tree: Node) -> None:
|
|
12
|
+
cache_path = get_cache_path(root_path)
|
|
13
|
+
# with open(cache_path, "w") as f:
|
|
14
|
+
# f.write(tree.model_dump_json(indent=2, by_alias=True))
|
|
15
|
+
# print(f"✅ Saved scan to {cache_path}")
|
|
16
|
+
|
|
17
|
+
def load_analysis(root_path: Path) -> Optional[Node]:
|
|
18
|
+
return None
|
|
19
|
+
|