Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions jscan/npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ This package is now a thin wrapper that runs the polyscan CLI, so existing
`npx jscan` invocations keep working while you migrate. It prints a
deprecation notice and forwards every command to polyscan, translating the
retired `--json`, `--text` and `--html` shorthands to `--format`. `--config`
exits with a hint instead, because polyscan discovers the file itself.
exits with a hint instead, because polyscan reads `.polyscan.toml` and no
longer reads `jscan.config.json`.

## Migrating

Expand All @@ -24,7 +25,8 @@ exits with a hint instead, because polyscan discovers the file itself.
| `jscan analyze --json src/` | `polyscan analyze --format json src/` |
| `jscan check src/` | `polyscan check src/` |
| `jscan deps src/` | `polyscan analyze --select deps src/` |
| `jscan init` | Retired — polyscan still reads `jscan.config.json` when present |
| `jscan init` | Retired. Write a `.polyscan.toml` by hand |
| `jscan.config.json` | `.polyscan.toml`; polyscan fails on a `jscan.config.json` until it is moved |

## Documentation

Expand Down
5 changes: 3 additions & 2 deletions jscan/npm/bin/jscan
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const args = process.argv.slice(2);
const retired = {
check: "run `npx polyscan check` instead; see its --help for the flags.",
deps: "run `polyscan analyze --select deps` for dependency analysis.",
init: "polyscan still reads jscan.config.json when present; write it by hand.",
init: "polyscan reads .polyscan.toml instead of jscan.config.json; write it by hand.",
};

if (args.length > 0 && Object.prototype.hasOwnProperty.call(retired, args[0])) {
Expand All @@ -44,7 +44,8 @@ function translateAnalyzeFlags(args) {
} else if (arg === "-c" || arg === "--config" || arg.startsWith("--config=")) {
console.error(
"jscan analyze --config was retired when jscan merged into polyscan; " +
"polyscan finds jscan.config.json by searching upward from the analyzed path."
"polyscan reads .polyscan.toml instead of jscan.config.json. " +
"Move the settings there and run `npx polyscan analyze`."
);
process.exit(2);
} else {
Expand Down
3 changes: 3 additions & 0 deletions polyscan/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- `polyscan check` checks the code against thresholds for a CI quality gate. It exits 0 when it finds no issues, 1 when it finds quality issues, and 2 when the analysis fails, including on an invalid flag. By default it fails on a function with cyclomatic complexity above 10, on critical JavaScript/TypeScript dead code, and on any circular dependency. A file that cannot be read or parsed also fails it. `--select`, `--max-complexity`, `--max-cycles`, `--allow-dead-code`, `--allow-circular-deps`, `--allow-parse-errors` and `--quiet` adjust the check (#182)
- One configuration file, `.polyscan.toml`, configures every language. polyscan reads the nearest one in the first analyzed path or a directory above it, or the file given with the new `--config`/`-c` flag of `analyze` and `check`. `[analysis]` sets `exclude` and `include_tests`, `[complexity]` sets the risk thresholds, and `[check]` sets the thresholds of `polyscan check`. A flag takes precedence over the file, and `--exclude` adds to its patterns. An unknown key, an invalid value or a malformed exclude pattern fails the run, and so does a malformed `--exclude` pattern, which was ignored before (#183)

### Changed

- polyscan no longer reads the jscan configuration files, such as `jscan.config.json`, or the `JSCAN_CONFIG` variable. A run that finds one before any `.polyscan.toml` fails with a message that names the file. Move `complexity.low_threshold` and `complexity.medium_threshold` to `.polyscan.toml` as they are, and move the `analysis.exclude_patterns` entries beyond the built-in list to `analysis.exclude`. The other jscan keys have no replacement in the file. They include `analysis.include_patterns`, `analysis.recursive`, `output.min_complexity`, `output.sort_by`, `dead_code.min_severity` and `dead_code.sort_by`. Use `--min-complexity` in place of `output.min_complexity` (#183)
- The complexity risk thresholds apply to Go, Rust and C++ as well as to JavaScript/TypeScript. Before, Go, Rust and C++ always used the defaults of 9 and 19 (#183)
- A JavaScript/TypeScript file that cannot be read or parsed no longer makes `analyze` print a JavaScript clone analysis error. The file is still listed under errors and charged to the health score, as it is for the other analyses

### Fixed
Expand Down
27 changes: 25 additions & 2 deletions polyscan/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,35 @@ polyscan analyze --exclude 'src/generated/**' .
polyscan analyze --include-tests .
```

`--select` takes any of `complexity`, `deadcode`, `clone`, `cbo`, `lcom` and `deps` (default: all); `deps` exists for Go and JavaScript/TypeScript, `cbo` for Go, Rust and JavaScript/TypeScript, `lcom` for Go and Rust, `deadcode` for JavaScript/TypeScript only, and a deselected or missing dimension is left out of the health score. JavaScript/TypeScript honors a `jscan.config.json` when the project has one. The JSON output is one document for every language, with `language` on every function and clone fragment.
`--select` takes any of `complexity`, `deadcode`, `clone`, `cbo`, `lcom` and `deps` (default: all); `deps` exists for Go and JavaScript/TypeScript, `cbo` for Go, Rust and JavaScript/TypeScript, `lcom` for Go and Rust, `deadcode` for JavaScript/TypeScript only, and a deselected or missing dimension is left out of the health score. The JSON output is one document for every language, with `language` on every function and clone fragment.

Version control, dependency and build output directories are not walked: any directory whose name starts with a dot, and `node_modules`, `vendor`, `target`, `build`, `dist` and `third_party`. A path named on the command line is analyzed whatever it is called. `--exclude` leaves out further files and directories, for every language and every analysis: a pattern without a slash is a glob matched against the file name and against each directory on the path, so `fixtures` drops every `fixtures` directory; a pattern with a slash is matched against the path relative to the analyzed directory, with `**` standing for any number of segments, so `src/generated/**` drops that tree. The flag takes a comma-separated list or may be repeated, and for JavaScript/TypeScript its patterns are added to the `exclude_patterns` of `jscan.config.json`.
Version control, dependency and build output directories are not walked: any directory whose name starts with a dot, and `node_modules`, `vendor`, `target`, `build`, `dist` and `third_party`. A path named on the command line is analyzed whatever it is called. `--exclude` leaves out further files and directories, for every language and every analysis: a pattern without a slash is a glob matched against the file name and against each directory on the path, so `fixtures` drops every `fixtures` directory; a pattern with a slash is matched against the path relative to the analyzed directory, with `**` standing for any number of segments, so `src/generated/**` drops that tree. The flag takes a comma-separated list or may be repeated.

Test files and test code are left out of every analysis unless `--include-tests` is given. For Go that is `*_test.go`; for C++ it is `*_test.*`, `*_tests.*`, `test_*.*` and `*Test.*` source files and any `test` or `tests` directory; for Rust it is `#[test]` functions, items under `#[cfg(test)]` or `#[cfg(all(test, ...))]`, `tests.rs` and `*_tests.rs` files and any `tests` directory, the conventional homes of a test module split into its own file and of Cargo's integration tests; for JavaScript/TypeScript it is `*.test.*` and `*.spec.*` files and any `__tests__` directory. With `--include-tests` the test code is analyzed for complexity and dead code, but it still stays out of clone detection, cohesion, coupling and dependency analysis: test functions share a skeleton by convention, and a test's types and imports describe the tests, not the package. A file that cannot be read is skipped and listed under `Errors`. A file with a syntax error is analyzed without the functions that contain the error, counted as partial, and listed under `Warnings`; C++ libraries hit this routinely, because a macro that opens a namespace or declares an attribute is a syntax error without the preprocessor.

## Configuration

One `.polyscan.toml` configures every language. polyscan reads the nearest one in the first analyzed path or a directory above it, or the file given with `--config`/`-c`. A flag takes precedence over the file, and `--exclude` adds to its patterns. An unknown key or an invalid value fails the run.

```toml
[analysis]
exclude = ["src/generated/**"] # added to the built-in skips
include_tests = false

[complexity]
low_threshold = 9 # highest complexity of the low risk band
medium_threshold = 19 # highest complexity of the medium risk band

[check] # thresholds of polyscan check
max_complexity = 10
max_cycles = 0
allow_dead_code = false
allow_circular_deps = false
allow_parse_errors = false
```

The jscan configuration files, such as `jscan.config.json`, are no longer read. A run that finds one before any `.polyscan.toml` fails and names the file.

## Complexity

Cyclomatic complexity is one plus the number of decision points in a function, counted the way `core/cfg` counts them on a control flow graph:
Expand Down
72 changes: 33 additions & 39 deletions polyscan/cmd/polyscan/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/ludo-technologies/polyscan/core/util"
"github.com/ludo-technologies/polyscan/polyscan/internal/analysis"
"github.com/ludo-technologies/polyscan/polyscan/internal/config"
"github.com/ludo-technologies/polyscan/polyscan/internal/js"
jsdomain "github.com/ludo-technologies/polyscan/polyscan/internal/js/domain"
"github.com/ludo-technologies/polyscan/polyscan/internal/js/service"
Expand Down Expand Up @@ -41,8 +42,7 @@ func analyzeCmd() *cobra.Command {
outputPath string
noOpen bool
minComplexity int
exclude []string
includeTests bool
files fileFlags
)

cmd := &cobra.Command{
Expand All @@ -65,6 +65,10 @@ is given: Go *_test.go; Rust #[test] functions, #[cfg(test)] items, tests.rs,
*_tests.rs and tests/; C++ *_test.*, *_tests.*, test_*.*, *Test.*, test/ and
tests/; JavaScript/TypeScript *.test.*, *.spec.* and __tests__/.

Settings are read from the nearest ` + config.FileName + ` in the first path or a
directory above it, or from the file given with --config. A flag takes
precedence over the file, and --exclude adds to its patterns.

By default, generates an HTML report and opens it in your browser.

Examples:
Expand All @@ -75,7 +79,8 @@ Examples:
polyscan analyze --select clone . # Clone detection only
polyscan analyze --min-complexity 10 . # List only functions at or above 10
polyscan analyze --exclude 'src/generated/**' . # Leave a directory out of every analysis
polyscan analyze --include-tests . # Analyze test files and test code too`,
polyscan analyze --include-tests . # Analyze test files and test code too
polyscan analyze -c ci.polyscan.toml . # Read the settings from ci.polyscan.toml`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
outputFormat := jsdomain.OutputFormat(format)
Expand All @@ -91,10 +96,13 @@ Examples:
if err != nil {
return err
}
options.IncludeTests = includeTests
cfg, err := files.load(cmd, args[0])
if err != nil {
return err
}

start := time.Now()
results, failures, err := runAnalyses(args, options, selection, exclude, includeTests, cmd.ErrOrStderr())
results, failures, err := runAnalyses(args, options, selection, cfg)
if err != nil {
return err
}
Expand Down Expand Up @@ -164,37 +172,33 @@ Examples:
cmd.Flags().StringVarP(&outputPath, "output", "o", "", "Report path (HTML default: "+defaultReportPath+"; JSON/text default: stdout)")
cmd.Flags().BoolVar(&noOpen, "no-open", false, "Don't open the HTML report in the browser")
cmd.Flags().IntVar(&minComplexity, "min-complexity", 1, "List only functions with at least this complexity")
addFileFlags(cmd, &exclude, &includeTests)
files.add(cmd)
return cmd
}

// addFileFlags adds the flags that choose which files are analyzed.
func addFileFlags(cmd *cobra.Command, exclude *[]string, includeTests *bool) {
cmd.Flags().StringSliceVar(exclude, "exclude", nil,
"Files and directories to leave out (comma-separated or repeated): a glob\n"+
"without a slash matches a file name or a directory anywhere on the path,\n"+
"one with a slash matches a path relative to the analyzed directory, with\n"+
"** for any number of segments (e.g. 'fixtures', 'src/generated/**')")
cmd.Flags().BoolVar(includeTests, "include-tests", false, "Analyze test files and test code, which are left out by default")
}

// runAnalyses runs the selected analyses over paths with the generic engine
// and the JavaScript/TypeScript pipeline and combines them into one result
// set. failures lists the JavaScript/TypeScript analyses that failed, which
// the results leave out; the caller decides whether that is fatal.
func runAnalyses(paths []string, options analysis.Options, selection js.Selection, exclude []string, includeTests bool, warn io.Writer) (jsdomain.AnalysisResults, []error, error) {
// and the JavaScript/TypeScript pipeline, both configured by cfg, and
// combines them into one result set. failures lists the JavaScript/TypeScript
// analyses that failed, which the results leave out; the caller decides
// whether that is fatal.
func runAnalyses(paths []string, options analysis.Options, selection js.Selection, cfg *config.Config) (jsdomain.AnalysisResults, []error, error) {
options.IncludeTests = cfg.Analysis.IncludeTests
options.ComplexityThresholds = analysis.ComplexityThresholds{
Low: cfg.Complexity.LowThreshold,
Medium: cfg.Complexity.MediumThreshold,
}
var generic *analysis.Report
var err error
if options != (analysis.Options{}) {
generic, err = analysis.Analyze(paths, options, exclude)
if options.Selected() {
generic, err = analysis.Analyze(paths, options, cfg.Analysis.Exclude)
if err != nil && !errors.Is(err, analysis.ErrNoFiles) {
return jsdomain.AnalysisResults{}, nil, err
}
}
var javascript *js.Result
var failures []error
if selection != (js.Selection{}) {
javascript, err = analyzeJavaScript(paths, selection, exclude, includeTests, warn)
javascript, err = analyzeJavaScript(paths, selection, cfg)
if err != nil {
return jsdomain.AnalysisResults{}, nil, err
}
Expand All @@ -214,36 +218,26 @@ func runAnalyses(paths []string, options analysis.Options, selection js.Selectio

// analyzeJavaScript runs the selected jscan analyses over the JavaScript/
// TypeScript files under paths, or returns nil when there are none. The
// files are collected with jscan's own configuration discovery and
// exclusion rules, so a JavaScript project keeps exactly the analysis
// jscan gave it, with the command line's exclude patterns added to the
// configuration's own.
//
// A tree without JavaScript skips the pipeline before configuration
// discovery, so a jscan configuration that would not load cannot fail the
// other languages' analysis.
func analyzeJavaScript(paths []string, selection js.Selection, exclude []string, includeTests bool, warn io.Writer) (*js.Result, error) {
// files are collected with jscan's own exclusion rules, with the configured
// exclude patterns added.
func analyzeJavaScript(paths []string, selection js.Selection, cfg *config.Config) (*js.Result, error) {
hasJS, err := js.ContainsFiles(paths)
if err != nil {
return nil, err
}
if !hasJS {
return nil, nil
}
cfg, err := js.LoadConfig("", paths[0], warn)
if err != nil {
return nil, fmt.Errorf("failed to load the JavaScript configuration: %w", err)
}
cfg.Analysis.ExcludePatterns = append(cfg.Analysis.ExcludePatterns, exclude...)
files, err := js.CollectFiles(paths, cfg, includeTests)
jsConfig := js.Config(cfg.Complexity.LowThreshold, cfg.Complexity.MediumThreshold, cfg.Analysis.Exclude)
files, err := js.CollectFiles(paths, jsConfig, cfg.Analysis.IncludeTests)
if err != nil {
return nil, err
}
if len(files) == 0 {
return nil, nil
}

return js.Run(context.Background(), files, cfg, selection), nil
return js.Run(context.Background(), files, jsConfig, selection), nil
}

// filterFunctions drops the listed functions below minComplexity. The summary
Expand Down
Loading
Loading