PyReprism 0.1.0__tar.gz → 0.2.0__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.
- {pyreprism-0.1.0 → pyreprism-0.2.0}/CHANGELOG.md +41 -1
- {pyreprism-0.1.0 → pyreprism-0.2.0}/PKG-INFO +77 -30
- {pyreprism-0.1.0 → pyreprism-0.2.0}/README.md +50 -0
- pyreprism-0.2.0/docs/intro.rst +100 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/__init__.py +44 -3
- pyreprism-0.2.0/src/PyReprism/_tokenops.py +153 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/cli.py +117 -3
- pyreprism-0.2.0/src/PyReprism/fingerprints.py +145 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/base.py +38 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/bro.py +1 -1
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/coffeescript.py +1 -1
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/eiffel.py +3 -2
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/gedcom.py +1 -1
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/gherkin.py +1 -1
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/io.py +1 -1
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/nix.py +1 -1
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/smalltalk.py +7 -1
- pyreprism-0.2.0/src/PyReprism/metrics.py +101 -0
- pyreprism-0.2.0/src/PyReprism/ngrams.py +169 -0
- pyreprism-0.1.0/docs/intro.rst +0 -53
- pyreprism-0.1.0/src/PyReprism/_tokenops.py +0 -78
- pyreprism-0.1.0/src/PyReprism/metrics.py +0 -41
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.flake8 +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/FUNDING.yaml +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/scripts/changelog_notes.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/scripts/package_version.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/workflows/ci.yml +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/workflows/publish.yml +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.gitignore +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.pre-commit-hooks.yaml +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/.readthedocs.yaml +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/CONTRIBUTING.md +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/LICENSE +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/RELEASING.md +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/codecov.yml +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/pyproject.toml +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/__main__.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/batch.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/diffs.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/engines/__init__.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/engines/base.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/engines/pygments_engine.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/engines/regex_engine.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/__init__.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/abap.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/actionscript.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/ada.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/apacheconf.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/apl.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/applescript.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/arduino.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/arff.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/asciidoc.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/asm6502.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/aspnet.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/autohotkey.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/autoit.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/bash.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/basic.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/batch.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/bison.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/brainfuck.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/c.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/clike.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/clojure.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/cpp.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/crystal.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/csharp.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/csp.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/css.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/css_extras.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/d.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/dart.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/diff.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/django.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/docker.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/elixir.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/erb.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/erlang.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/flow.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/fortran.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/fsharp.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/git.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/glsl.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/go.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/graphql.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/groovy.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/haml.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/handlebars.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/haskell.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/haxe.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/hpkp.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/hsts.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/html.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/ichigojam.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/icon.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/inform7.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/ini.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/j.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/java.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/javascript.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/jolie.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/json.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/jsx.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/julia.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/keyman.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/kotlin.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/latex.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/less.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/liquid.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/livescript.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/lolcode.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/lua.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/makefile.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/markdown.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/markup.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/markup_templating.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/matlab.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/mel.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/mizar.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/monkey.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/n4js.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/nasm.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/nginx.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/nim.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/nsis.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/objectivec.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/ocaml.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/opencl.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/oz.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/parigp.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/parser.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/pascal.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/perl.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/php.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/php_extras.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/plsql.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/powershell.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/processing.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/prolog.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/properties.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/protobuf.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/pug.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/puppet.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/pure.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/python.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/q.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/qore.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/r.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/reason.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/registry.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/renpy.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/rest.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/rip.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/roboconf.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/ruby.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/rust.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/sas.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/sass.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/scala.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/scheme.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/scss.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/smarty.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/soy.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/sql.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/stylus.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/swift.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/tcl.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/textile.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/tsx.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/twig.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/typescript.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/vbnet.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/velocity.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/verilog.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/vhdl.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/vim.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/visual_basic.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/wasam.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/xeora.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/xojo.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/yaml.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/py.typed +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/tokens.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/utils/__init__.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/utils/extension.py +0 -0
- {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/utils/normalizer.py +0 -0
|
@@ -7,7 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-07-08
|
|
11
|
+
|
|
12
|
+
This release adds a static code-analysis toolkit on top of the tokenizer:
|
|
13
|
+
diff/PR analysis, complexity metrics, n-gram "naturalness", and clone/plagiarism
|
|
14
|
+
similarity — plus a comprehensive per-language test suite that hardened comment
|
|
15
|
+
handling across the board.
|
|
16
|
+
|
|
10
17
|
### Added
|
|
18
|
+
- **Fingerprinting & similarity** (`PyReprism.fingerprints`): winnowing k-gram
|
|
19
|
+
fingerprints over the normalized token stream for clone/plagiarism detection —
|
|
20
|
+
`fingerprint()`, `similarity()`/`containment()` (rename-invariant by default),
|
|
21
|
+
and a `FingerprintIndex` for many-to-many detection over a corpus. CLI:
|
|
22
|
+
`pyreprism similarity a b` and `pyreprism clones DIR --threshold`.
|
|
23
|
+
- **N-gram analysis & code naturalness** (`PyReprism.ngrams`): token/type n-gram
|
|
24
|
+
extraction and frequency counts, plus an `NgramModel` (add-k smoothing,
|
|
25
|
+
save/load) that measures cross-entropy / perplexity against a trained corpus
|
|
26
|
+
("naturalness of software"). CLI: `pyreprism ngrams` and
|
|
27
|
+
`pyreprism perplexity --train`.
|
|
28
|
+
- **Complexity metrics** computed from the token stream: `halstead()`
|
|
29
|
+
(volume/difficulty/effort/bugs), `cyclomatic_complexity()` (approximate McCabe),
|
|
30
|
+
`maintainability_index()` (0–100), `max_nesting_depth()`, and `code_metrics()`
|
|
31
|
+
which bundles them with the line/token stats. Exposed on the CLI via
|
|
32
|
+
`pyreprism stats --full`.
|
|
11
33
|
- **Diff processing** (`PyReprism.diffs`): parse unified/`git` diffs and analyze
|
|
12
34
|
the changed code per file in its own language. Includes churn metrics
|
|
13
35
|
(`diff_stats`: added/removed split into code, comment and blank), cosmetic
|
|
@@ -17,6 +39,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
39
|
`old_source` enables accurate full-file classification. New CLI command
|
|
18
40
|
`pyreprism diff` (`--json` / `--csv` / `--per-file` / `--cosmetic`).
|
|
19
41
|
|
|
42
|
+
### Changed
|
|
43
|
+
- Adopted the standard `src/` layout with tests at the top level, consolidated
|
|
44
|
+
all dependencies into `pyproject.toml` extras, and rewrote the landing page.
|
|
45
|
+
- Single-sourced the version from the package and automated releases: pushing a
|
|
46
|
+
`vX.Y.Z` tag now publishes to PyPI (Trusted Publishing) and creates a GitHub
|
|
47
|
+
Release. Added Python 3.13 to the CI matrix.
|
|
48
|
+
- Added a comprehensive, data-driven per-language comment test suite that
|
|
49
|
+
requires every registered language to be covered.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- Fixed comment stripping in 8 more languages surfaced by the new test suite:
|
|
53
|
+
`smalltalk` (kept the comment and deleted the code) and
|
|
54
|
+
`eiffel`/`bro`/`coffeescript`/`io`/`nix`/`gherkin`/`gedcom` (trailing comments
|
|
55
|
+
not removed and newlines dropped).
|
|
56
|
+
- Fixed a CLI argument-ordering incompatibility on Python ≤ 3.11 (options must
|
|
57
|
+
follow positionals).
|
|
58
|
+
|
|
20
59
|
## [0.1.0] - 2026-07-07
|
|
21
60
|
|
|
22
61
|
This release turns PyReprism from a comment-removal helper into a full
|
|
@@ -73,6 +112,7 @@ ML-oriented normalization, an optional accurate backend, and batch processing.
|
|
|
73
112
|
- Early beta releases: comment removal for an initial set of languages and the
|
|
74
113
|
`Normalizer` whitespace helper.
|
|
75
114
|
|
|
76
|
-
[Unreleased]: https://github.com/unlv-evol/PyReprism/compare/v0.
|
|
115
|
+
[Unreleased]: https://github.com/unlv-evol/PyReprism/compare/v0.2.0...HEAD
|
|
116
|
+
[0.2.0]: https://github.com/unlv-evol/PyReprism/compare/v0.1.0...v0.2.0
|
|
77
117
|
[0.1.0]: https://github.com/unlv-evol/PyReprism/releases/tag/v0.1.0
|
|
78
118
|
[0.0.4]: https://github.com/unlv-evol/PyReprism/releases/tag/v0.0.4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyReprism
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Framework for Source Code Preprocessing
|
|
5
5
|
Project-URL: Homepage, https://github.com/unlv-evol/PyReprism
|
|
6
6
|
Project-URL: Documentation, https://pyreprism.readthedocs.io
|
|
@@ -58,54 +58,101 @@ Description-Content-Type: text/x-rst
|
|
|
58
58
|
|
|
59
59
|
.. _intro_toplevel:
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
Overview
|
|
63
|
-
|
|
61
|
+
========
|
|
62
|
+
Overview
|
|
63
|
+
========
|
|
64
64
|
|
|
65
|
-
PyReprism is a Python framework
|
|
65
|
+
**PyReprism** is a Python framework for source-code preprocessing. It lets you
|
|
66
|
+
**match, extract, count, and remove** comments, strings, numbers, operators,
|
|
67
|
+
keywords and other language-specific constructs across **145+ programming
|
|
68
|
+
languages and file formats** — through a small high-level API, a command-line
|
|
69
|
+
tool, code metrics, ML-oriented normalization, and diff/pull-request analysis.
|
|
66
70
|
|
|
71
|
+
Highlights
|
|
72
|
+
==========
|
|
73
|
+
|
|
74
|
+
* One consistent API for every language: ``remove_*`` / ``extract_*`` /
|
|
75
|
+
``count_*`` / ``match_*`` for each construct, plus a lossless ``tokenize()``.
|
|
76
|
+
* Code metrics (``stats``) and ML canonicalization (``normalize``) for
|
|
77
|
+
clone/plagiarism detection and code-embedding pipelines.
|
|
78
|
+
* Optional high-accuracy `Pygments`_ backend — with **zero required
|
|
79
|
+
dependencies** by default.
|
|
80
|
+
* Batch/corpus analysis and unified/``git`` diff processing.
|
|
81
|
+
* A ``pyreprism`` command-line tool (also ``python -m PyReprism``).
|
|
67
82
|
|
|
68
83
|
Requirements
|
|
69
84
|
============
|
|
70
85
|
|
|
71
|
-
* `Python`_ 3.
|
|
72
|
-
* `Git`_
|
|
86
|
+
* `Python`_ 3.8 or newer
|
|
73
87
|
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
Install
|
|
89
|
+
=======
|
|
76
90
|
|
|
77
|
-
|
|
78
|
-
====================
|
|
91
|
+
.. code-block:: shell
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
pip install PyReprism
|
|
81
94
|
|
|
82
|
-
|
|
95
|
+
For the higher-accuracy tokenizer backend:
|
|
83
96
|
|
|
84
|
-
..
|
|
97
|
+
.. code-block:: shell
|
|
85
98
|
|
|
86
|
-
|
|
99
|
+
pip install "PyReprism[accurate]"
|
|
87
100
|
|
|
88
|
-
|
|
101
|
+
Quick start
|
|
89
102
|
===========
|
|
90
103
|
|
|
91
|
-
|
|
104
|
+
.. code-block:: python
|
|
105
|
+
|
|
106
|
+
import PyReprism as pr
|
|
107
|
+
|
|
108
|
+
source = """
|
|
109
|
+
# a comment
|
|
110
|
+
x = 5 + 6
|
|
111
|
+
print(x) # inline
|
|
112
|
+
"""
|
|
92
113
|
|
|
93
|
-
|
|
114
|
+
pr.remove_comments(source, lang="python") # code without comments
|
|
115
|
+
pr.extract_comments(source, lang="python") # ['# a comment', '# inline']
|
|
116
|
+
pr.count_comments(source, lang="python") # 2
|
|
94
117
|
|
|
95
|
-
|
|
118
|
+
``lang`` accepts a language name (``"python"``), a file extension (``".py"``),
|
|
119
|
+
or a language class. Canonicalize code for machine learning, or measure it:
|
|
120
|
+
|
|
121
|
+
.. code-block:: python
|
|
122
|
+
|
|
123
|
+
pr.normalize("total = price * 42", lang="python") # 'VAR1 = VAR2 * 0'
|
|
124
|
+
pr.stats(source, lang="python").comment_to_code_ratio
|
|
125
|
+
|
|
126
|
+
Command line
|
|
127
|
+
============
|
|
96
128
|
|
|
97
|
-
|
|
98
|
-
$ cd PyReprism
|
|
129
|
+
.. code-block:: shell
|
|
99
130
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
$ source venv/bin/activate
|
|
131
|
+
pyreprism remove comments file.py
|
|
132
|
+
pyreprism scan myproject/ --csv # aggregate code metrics over a tree
|
|
133
|
+
git diff | pyreprism diff --cosmetic # flag comment/whitespace-only changes
|
|
104
134
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
$ pip install -r requirements.txt
|
|
135
|
+
Documentation
|
|
136
|
+
=============
|
|
108
137
|
|
|
109
|
-
|
|
138
|
+
Full documentation, including the API reference and the list of supported
|
|
139
|
+
languages, is available at https://pyreprism.readthedocs.io.
|
|
110
140
|
|
|
111
|
-
|
|
141
|
+
Source code
|
|
142
|
+
===========
|
|
143
|
+
|
|
144
|
+
PyReprism is developed on GitHub at
|
|
145
|
+
https://github.com/unlv-evol/PyReprism. To work on it locally:
|
|
146
|
+
|
|
147
|
+
.. code-block:: shell
|
|
148
|
+
|
|
149
|
+
git clone https://github.com/unlv-evol/PyReprism.git
|
|
150
|
+
cd PyReprism
|
|
151
|
+
python3 -m venv venv && source venv/bin/activate
|
|
152
|
+
pip install -e ".[dev]"
|
|
153
|
+
pytest
|
|
154
|
+
|
|
155
|
+
See ``CONTRIBUTING.md`` for contribution guidelines.
|
|
156
|
+
|
|
157
|
+
.. _Python: https://www.python.org
|
|
158
|
+
.. _Pygments: https://pygments.org
|
|
@@ -87,6 +87,56 @@ s.comment_to_code_ratio, s.comment_density
|
|
|
87
87
|
s.as_dict() # includes per-token-type counts, ready for JSON / dataframes
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
Complexity metrics (computed from the token stream):
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
pr.halstead(source, lang="python").volume # Halstead volume/difficulty/effort/bugs
|
|
94
|
+
pr.cyclomatic_complexity(source, lang="python") # approximate McCabe complexity
|
|
95
|
+
pr.maintainability_index(source, lang="python") # 0–100 (higher is better)
|
|
96
|
+
pr.code_metrics(source, lang="python") # everything above in one dict
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
On the CLI: `pyreprism stats --full file.py` (add `--json` for machine output).
|
|
100
|
+
|
|
101
|
+
### Similarity & clone / plagiarism detection
|
|
102
|
+
|
|
103
|
+
Winnowing k-gram fingerprints over the *normalized* token stream, so matches
|
|
104
|
+
survive variable renaming and literal changes (Type-2 clones):
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from PyReprism import fingerprints as fp
|
|
108
|
+
|
|
109
|
+
fp.similarity(code_a, code_b, "python") # 0.0–1.0 (Jaccard of fingerprints)
|
|
110
|
+
fp.containment(code_a, code_b, "python") # how much of A appears in B
|
|
111
|
+
|
|
112
|
+
index = fp.FingerprintIndex() # many-to-many clone detection
|
|
113
|
+
index.add_paths("submissions/")
|
|
114
|
+
index.similar_pairs(threshold=0.7) # -> [(file_a, file_b, score), ...]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
On the CLI: `pyreprism similarity a.py b.py` and
|
|
118
|
+
`pyreprism clones submissions/ --threshold 0.7`.
|
|
119
|
+
|
|
120
|
+
### N-grams & code "naturalness"
|
|
121
|
+
|
|
122
|
+
Token n-grams (over token text or, structurally, over token *types*) and an
|
|
123
|
+
n-gram language model that measures how predictable/"natural" code is
|
|
124
|
+
(Hindle et al.):
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from PyReprism import ngrams
|
|
128
|
+
|
|
129
|
+
ngrams.ngram_counts(source, "python", n=3).most_common(10)
|
|
130
|
+
ngrams.ngrams(source, "python", n=2, types=True) # structural n-grams
|
|
131
|
+
|
|
132
|
+
model = ngrams.train("corpus/", n=3) # train on a code corpus
|
|
133
|
+
model.perplexity(ngrams.token_sequence(source, "python")) # lower = more natural
|
|
134
|
+
model.save("model.json")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
On the CLI: `pyreprism ngrams file.py -n 3 --top 20` and
|
|
138
|
+
`pyreprism perplexity --train corpus/ file.py`.
|
|
139
|
+
|
|
90
140
|
### Normalization for ML / clone detection
|
|
91
141
|
|
|
92
142
|
Canonicalize code so that only its structure remains — rename identifiers to
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
.. _intro_toplevel:
|
|
2
|
+
|
|
3
|
+
========
|
|
4
|
+
Overview
|
|
5
|
+
========
|
|
6
|
+
|
|
7
|
+
**PyReprism** is a Python framework for source-code preprocessing. It lets you
|
|
8
|
+
**match, extract, count, and remove** comments, strings, numbers, operators,
|
|
9
|
+
keywords and other language-specific constructs across **145+ programming
|
|
10
|
+
languages and file formats** — through a small high-level API, a command-line
|
|
11
|
+
tool, code metrics, ML-oriented normalization, and diff/pull-request analysis.
|
|
12
|
+
|
|
13
|
+
Highlights
|
|
14
|
+
==========
|
|
15
|
+
|
|
16
|
+
* One consistent API for every language: ``remove_*`` / ``extract_*`` /
|
|
17
|
+
``count_*`` / ``match_*`` for each construct, plus a lossless ``tokenize()``.
|
|
18
|
+
* Code metrics (``stats``) and ML canonicalization (``normalize``) for
|
|
19
|
+
clone/plagiarism detection and code-embedding pipelines.
|
|
20
|
+
* Optional high-accuracy `Pygments`_ backend — with **zero required
|
|
21
|
+
dependencies** by default.
|
|
22
|
+
* Batch/corpus analysis and unified/``git`` diff processing.
|
|
23
|
+
* A ``pyreprism`` command-line tool (also ``python -m PyReprism``).
|
|
24
|
+
|
|
25
|
+
Requirements
|
|
26
|
+
============
|
|
27
|
+
|
|
28
|
+
* `Python`_ 3.8 or newer
|
|
29
|
+
|
|
30
|
+
Install
|
|
31
|
+
=======
|
|
32
|
+
|
|
33
|
+
.. code-block:: shell
|
|
34
|
+
|
|
35
|
+
pip install PyReprism
|
|
36
|
+
|
|
37
|
+
For the higher-accuracy tokenizer backend:
|
|
38
|
+
|
|
39
|
+
.. code-block:: shell
|
|
40
|
+
|
|
41
|
+
pip install "PyReprism[accurate]"
|
|
42
|
+
|
|
43
|
+
Quick start
|
|
44
|
+
===========
|
|
45
|
+
|
|
46
|
+
.. code-block:: python
|
|
47
|
+
|
|
48
|
+
import PyReprism as pr
|
|
49
|
+
|
|
50
|
+
source = """
|
|
51
|
+
# a comment
|
|
52
|
+
x = 5 + 6
|
|
53
|
+
print(x) # inline
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
pr.remove_comments(source, lang="python") # code without comments
|
|
57
|
+
pr.extract_comments(source, lang="python") # ['# a comment', '# inline']
|
|
58
|
+
pr.count_comments(source, lang="python") # 2
|
|
59
|
+
|
|
60
|
+
``lang`` accepts a language name (``"python"``), a file extension (``".py"``),
|
|
61
|
+
or a language class. Canonicalize code for machine learning, or measure it:
|
|
62
|
+
|
|
63
|
+
.. code-block:: python
|
|
64
|
+
|
|
65
|
+
pr.normalize("total = price * 42", lang="python") # 'VAR1 = VAR2 * 0'
|
|
66
|
+
pr.stats(source, lang="python").comment_to_code_ratio
|
|
67
|
+
|
|
68
|
+
Command line
|
|
69
|
+
============
|
|
70
|
+
|
|
71
|
+
.. code-block:: shell
|
|
72
|
+
|
|
73
|
+
pyreprism remove comments file.py
|
|
74
|
+
pyreprism scan myproject/ --csv # aggregate code metrics over a tree
|
|
75
|
+
git diff | pyreprism diff --cosmetic # flag comment/whitespace-only changes
|
|
76
|
+
|
|
77
|
+
Documentation
|
|
78
|
+
=============
|
|
79
|
+
|
|
80
|
+
Full documentation, including the API reference and the list of supported
|
|
81
|
+
languages, is available at https://pyreprism.readthedocs.io.
|
|
82
|
+
|
|
83
|
+
Source code
|
|
84
|
+
===========
|
|
85
|
+
|
|
86
|
+
PyReprism is developed on GitHub at
|
|
87
|
+
https://github.com/unlv-evol/PyReprism. To work on it locally:
|
|
88
|
+
|
|
89
|
+
.. code-block:: shell
|
|
90
|
+
|
|
91
|
+
git clone https://github.com/unlv-evol/PyReprism.git
|
|
92
|
+
cd PyReprism
|
|
93
|
+
python3 -m venv venv && source venv/bin/activate
|
|
94
|
+
pip install -e ".[dev]"
|
|
95
|
+
pytest
|
|
96
|
+
|
|
97
|
+
See ``CONTRIBUTING.md`` for contribution guidelines.
|
|
98
|
+
|
|
99
|
+
.. _Python: https://www.python.org
|
|
100
|
+
.. _Pygments: https://pygments.org
|
|
@@ -17,11 +17,11 @@ import importlib
|
|
|
17
17
|
import os
|
|
18
18
|
from typing import List, Optional, Sequence, Type, Union
|
|
19
19
|
|
|
20
|
-
from .metrics import CodeStats
|
|
20
|
+
from .metrics import CodeStats, Halstead
|
|
21
21
|
from .tokens import Token, TokenType
|
|
22
22
|
from .utils.normalizer import Normalizer
|
|
23
23
|
|
|
24
|
-
__version__ = "0.
|
|
24
|
+
__version__ = "0.2.0"
|
|
25
25
|
|
|
26
26
|
LanguageLike = Union[str, "type"]
|
|
27
27
|
|
|
@@ -274,6 +274,46 @@ def stats(source: str, lang: LanguageLike, engine: str = 'regex') -> CodeStats:
|
|
|
274
274
|
return _tokenops.stats(_engine_tokens(source, lang, engine), source)
|
|
275
275
|
|
|
276
276
|
|
|
277
|
+
def halstead(source: str, lang: LanguageLike, engine: str = 'regex') -> Halstead:
|
|
278
|
+
"""Return the :class:`Halstead` complexity measures for ``source``."""
|
|
279
|
+
if _is_regex(engine):
|
|
280
|
+
return _resolve(lang).halstead(source)
|
|
281
|
+
from . import _tokenops
|
|
282
|
+
return _tokenops.halstead(_engine_tokens(source, lang, engine))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def cyclomatic_complexity(source: str, lang: LanguageLike, engine: str = 'regex') -> int:
|
|
286
|
+
"""Approximate McCabe cyclomatic complexity (token-based)."""
|
|
287
|
+
if _is_regex(engine):
|
|
288
|
+
return _resolve(lang).cyclomatic_complexity(source)
|
|
289
|
+
from . import _tokenops
|
|
290
|
+
return _tokenops.cyclomatic(_engine_tokens(source, lang, engine))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def maintainability_index(source: str, lang: LanguageLike, engine: str = 'regex') -> float:
|
|
294
|
+
"""SEI-normalized Maintainability Index in ``[0, 100]`` (higher is better)."""
|
|
295
|
+
if _is_regex(engine):
|
|
296
|
+
return _resolve(lang).maintainability_index(source)
|
|
297
|
+
from . import _tokenops
|
|
298
|
+
tokens = _engine_tokens(source, lang, engine)
|
|
299
|
+
return _tokenops.maintainability_index(tokens, _tokenops.stats(tokens, source).code_lines)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def code_metrics(source: str, lang: LanguageLike, engine: str = 'regex') -> dict:
|
|
303
|
+
"""Return a combined metrics dict (line stats + Halstead + complexity + MI)."""
|
|
304
|
+
if _is_regex(engine):
|
|
305
|
+
return _resolve(lang).code_metrics(source)
|
|
306
|
+
from . import _tokenops
|
|
307
|
+
tokens = _engine_tokens(source, lang, engine)
|
|
308
|
+
stats_obj = _tokenops.stats(tokens, source)
|
|
309
|
+
data = stats_obj.as_dict()
|
|
310
|
+
data['halstead'] = _tokenops.halstead(tokens).as_dict()
|
|
311
|
+
data['cyclomatic_complexity'] = _tokenops.cyclomatic(tokens)
|
|
312
|
+
data['max_nesting_depth'] = _tokenops.max_nesting_depth(tokens)
|
|
313
|
+
data['maintainability_index'] = _tokenops.maintainability_index(tokens, stats_obj.code_lines)
|
|
314
|
+
return data
|
|
315
|
+
|
|
316
|
+
|
|
277
317
|
def normalize(source: str, lang: LanguageLike, engine: str = 'regex', **options) -> str:
|
|
278
318
|
"""Return a canonicalized form of ``source`` for ML / clone detection.
|
|
279
319
|
|
|
@@ -329,7 +369,7 @@ def preprocess(source: str, lang: LanguageLike, steps: Sequence[str] = ('comment
|
|
|
329
369
|
|
|
330
370
|
__all__ = [
|
|
331
371
|
'__version__',
|
|
332
|
-
'Token', 'TokenType', 'CodeStats', 'Normalizer',
|
|
372
|
+
'Token', 'TokenType', 'CodeStats', 'Halstead', 'Normalizer',
|
|
333
373
|
'get_language', 'detect_language',
|
|
334
374
|
'remove_comments', 'extract_comments', 'count_comments', 'match_comments',
|
|
335
375
|
'remove_keywords', 'extract_keywords', 'count_keywords',
|
|
@@ -338,5 +378,6 @@ __all__ = [
|
|
|
338
378
|
'remove_strings', 'extract_strings',
|
|
339
379
|
'extract_identifiers', 'remove_whitespaces',
|
|
340
380
|
'blank_comments', 'stats', 'normalize',
|
|
381
|
+
'halstead', 'cyclomatic_complexity', 'maintainability_index', 'code_metrics',
|
|
341
382
|
'tokenize', 'preprocess',
|
|
342
383
|
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Generic operations derived from a token stream.
|
|
2
|
+
|
|
3
|
+
These work on any ``List[Token]`` regardless of which engine produced it, so the
|
|
4
|
+
regex and pygments backends share one implementation of remove/extract/count,
|
|
5
|
+
normalization and metrics.
|
|
6
|
+
"""
|
|
7
|
+
import math
|
|
8
|
+
from collections import Counter
|
|
9
|
+
from typing import List, Set
|
|
10
|
+
|
|
11
|
+
from .metrics import CodeStats, Halstead
|
|
12
|
+
from .tokens import Token, TokenType
|
|
13
|
+
|
|
14
|
+
# Language-agnostic decision points used for the approximate cyclomatic complexity.
|
|
15
|
+
DECISION_KEYWORDS = frozenset({
|
|
16
|
+
'if', 'elif', 'elseif', 'for', 'foreach', 'while', 'case', 'when', 'catch',
|
|
17
|
+
'except', 'and', 'or', 'unless',
|
|
18
|
+
})
|
|
19
|
+
# Substrings within operator tokens that add a decision (&&, ||, ternary ?).
|
|
20
|
+
DECISION_OPERATORS = ('&&', '||', '?')
|
|
21
|
+
|
|
22
|
+
_OPERAND_TYPES = (TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.STRING)
|
|
23
|
+
_OPEN_BRACKETS, _CLOSE_BRACKETS = set('([{'), set(')]}')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def remove(tokens: List[Token], ttype: TokenType) -> str:
|
|
27
|
+
return ''.join(t.value for t in tokens if t.type is not ttype)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract(tokens: List[Token], ttype: TokenType) -> List[str]:
|
|
31
|
+
return [t.value for t in tokens if t.type is ttype]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def count(tokens: List[Token], ttype: TokenType) -> int:
|
|
35
|
+
return sum(1 for t in tokens if t.type is ttype)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def normalize(tokens: List[Token], *, rename_identifiers: bool = True,
|
|
39
|
+
mask_numbers: bool = True, mask_strings: bool = True,
|
|
40
|
+
drop_comments: bool = True, collapse_whitespace: bool = False,
|
|
41
|
+
identifier_prefix: str = 'VAR', number_placeholder: str = '0',
|
|
42
|
+
string_placeholder: str = '"STR"') -> str:
|
|
43
|
+
mapping = {}
|
|
44
|
+
parts = []
|
|
45
|
+
for tok in tokens:
|
|
46
|
+
t = tok.type
|
|
47
|
+
if t is TokenType.COMMENT:
|
|
48
|
+
if not drop_comments:
|
|
49
|
+
parts.append(tok.value)
|
|
50
|
+
elif t is TokenType.STRING and mask_strings:
|
|
51
|
+
parts.append(string_placeholder)
|
|
52
|
+
elif t is TokenType.NUMBER and mask_numbers:
|
|
53
|
+
parts.append(number_placeholder)
|
|
54
|
+
elif t is TokenType.IDENTIFIER and rename_identifiers:
|
|
55
|
+
name = mapping.get(tok.value)
|
|
56
|
+
if name is None:
|
|
57
|
+
name = f"{identifier_prefix}{len(mapping) + 1}"
|
|
58
|
+
mapping[tok.value] = name
|
|
59
|
+
parts.append(name)
|
|
60
|
+
elif t is TokenType.WHITESPACE and collapse_whitespace:
|
|
61
|
+
parts.append(' ')
|
|
62
|
+
else:
|
|
63
|
+
parts.append(tok.value)
|
|
64
|
+
return ''.join(parts)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def stats(tokens: List[Token], source: str) -> CodeStats:
|
|
68
|
+
counts = {t: 0 for t in TokenType}
|
|
69
|
+
code_lines = set()
|
|
70
|
+
comment_lines = set()
|
|
71
|
+
for tok in tokens:
|
|
72
|
+
counts[tok.type] += 1
|
|
73
|
+
if tok.type is TokenType.WHITESPACE:
|
|
74
|
+
continue
|
|
75
|
+
span = range(tok.line, tok.line + tok.value.count('\n') + 1)
|
|
76
|
+
target = comment_lines if tok.type is TokenType.COMMENT else code_lines
|
|
77
|
+
target.update(span)
|
|
78
|
+
total = len(source.splitlines())
|
|
79
|
+
code = len(code_lines)
|
|
80
|
+
comment = len(comment_lines - code_lines)
|
|
81
|
+
blank = max(total - code - comment, 0)
|
|
82
|
+
return CodeStats(
|
|
83
|
+
lines=total, code_lines=code, comment_lines=comment, blank_lines=blank,
|
|
84
|
+
characters=len(source),
|
|
85
|
+
comment_tokens=counts[TokenType.COMMENT],
|
|
86
|
+
string_tokens=counts[TokenType.STRING],
|
|
87
|
+
number_tokens=counts[TokenType.NUMBER],
|
|
88
|
+
keyword_tokens=counts[TokenType.KEYWORD],
|
|
89
|
+
identifier_tokens=counts[TokenType.IDENTIFIER],
|
|
90
|
+
operator_tokens=counts[TokenType.OPERATOR],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def halstead(tokens: List[Token]) -> Halstead:
|
|
95
|
+
"""Compute :class:`~PyReprism.metrics.Halstead` measures from ``tokens``."""
|
|
96
|
+
operators: Counter = Counter()
|
|
97
|
+
operands: Counter = Counter()
|
|
98
|
+
for tok in tokens:
|
|
99
|
+
if tok.type in (TokenType.OPERATOR, TokenType.KEYWORD):
|
|
100
|
+
operators[tok.value] += 1
|
|
101
|
+
elif tok.type is TokenType.OTHER and tok.value.strip():
|
|
102
|
+
operators[tok.value] += 1
|
|
103
|
+
elif tok.type in _OPERAND_TYPES:
|
|
104
|
+
operands[tok.value] += 1
|
|
105
|
+
return Halstead(
|
|
106
|
+
distinct_operators=len(operators), distinct_operands=len(operands),
|
|
107
|
+
total_operators=sum(operators.values()), total_operands=sum(operands.values()),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cyclomatic(tokens: List[Token], decision_keywords: Set[str] = DECISION_KEYWORDS) -> int:
|
|
112
|
+
"""Approximate McCabe cyclomatic complexity: ``1 + decision points``.
|
|
113
|
+
|
|
114
|
+
Counts branch keywords plus ``&&``/``||``/``?`` operators. This is a
|
|
115
|
+
token-level approximation; an exact value needs a control-flow graph.
|
|
116
|
+
"""
|
|
117
|
+
complexity = 1
|
|
118
|
+
for tok in tokens:
|
|
119
|
+
# Branch words are usually KEYWORD, but word-operators (and/or) can land
|
|
120
|
+
# as IDENTIFIER in the generic lexer, so check both.
|
|
121
|
+
if (tok.type in (TokenType.KEYWORD, TokenType.IDENTIFIER)
|
|
122
|
+
and tok.value.lower() in decision_keywords):
|
|
123
|
+
complexity += 1
|
|
124
|
+
elif tok.type is TokenType.OPERATOR:
|
|
125
|
+
for op in DECISION_OPERATORS:
|
|
126
|
+
complexity += tok.value.count(op)
|
|
127
|
+
return complexity
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def max_nesting_depth(tokens: List[Token]) -> int:
|
|
131
|
+
"""Maximum bracket nesting depth (``()``/``[]``/``{}``), ignoring strings/comments."""
|
|
132
|
+
depth = deepest = 0
|
|
133
|
+
for tok in tokens:
|
|
134
|
+
if tok.type in (TokenType.STRING, TokenType.COMMENT):
|
|
135
|
+
continue
|
|
136
|
+
for ch in tok.value:
|
|
137
|
+
if ch in _OPEN_BRACKETS:
|
|
138
|
+
depth += 1
|
|
139
|
+
deepest = max(deepest, depth)
|
|
140
|
+
elif ch in _CLOSE_BRACKETS:
|
|
141
|
+
depth = max(0, depth - 1)
|
|
142
|
+
return deepest
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def maintainability_index(tokens: List[Token], code_lines: int) -> float:
|
|
146
|
+
"""SEI-normalized Maintainability Index in ``[0, 100]`` (higher is better)."""
|
|
147
|
+
volume = halstead(tokens).volume
|
|
148
|
+
complexity = cyclomatic(tokens)
|
|
149
|
+
raw = (171
|
|
150
|
+
- 5.2 * math.log(max(volume, 1))
|
|
151
|
+
- 0.23 * complexity
|
|
152
|
+
- 16.2 * math.log(max(code_lines, 1)))
|
|
153
|
+
return round(max(0.0, min(100.0, raw * 100 / 171)), 2)
|