Giter VIP home page Giter VIP logo

necessist's Introduction

Necessist

Run tests with statements and method calls removed to help identify broken tests

Necessist currently supports Anchor (TS), Foundry, Go, Hardhat (TS), and Rust.

A paper on Necessist (Test Harness Mutilation) will appear in Mutation 2024. (slides, preprint)

Contents

Installation

System requirements:

Install pkg-config and sqlite3 development files on your system, e.g., on Ubuntu:

sudo apt install pkg-config libsqlite3-dev

Install Necessist from crates.io:

cargo install necessist

Install Necessist from github.com:

cargo install --git https://github.com/trailofbits/necessist --branch release

Overview

Necessist iteratively removes statements and method calls from tests and then runs them. If a test passes with a statement or method call removed, it could indicate a problem in the test. Or worse, it could indicate a problem in the code being tested.

Example

This example is from rust-openssl. The verify_untrusted_callback_override_ok test checks that a failed certificate validation can be overridden by a callback. But if the callback were never called (e.g., because of a failed connection), the test would still pass. Necessist reveals this fact by showing that the test passes without the call to set_verify_callback:

#[test]
fn verify_untrusted_callback_override_ok() {
    let server = Server::builder().build();

    let mut client = server.client();
    client
        .ctx()
        .set_verify_callback(SslVerifyMode::PEER, |_, x509| { //
            assert!(x509.current_cert().is_some());           // Test passes without this call
            true                                              // to `set_verify_callback`.
        });                                                   //

    client.connect();
}

Following this discovery, a flag was added to the test to record whether the callback is called. The flag must be set for the test to succeed:

#[test]
fn verify_untrusted_callback_override_ok() {
    static CALLED_BACK: AtomicBool = AtomicBool::new(false);  // Added

    let server = Server::builder().build();

    let mut client = server.client();
    client
        .ctx()
        .set_verify_callback(SslVerifyMode::PEER, |_, x509| {
            CALLED_BACK.store(true, Ordering::SeqCst);        // Added
            assert!(x509.current_cert().is_some());
            true
        });

    client.connect();
    assert!(CALLED_BACK.load(Ordering::SeqCst));              // Added
}

Comparison to conventional mutation testing

Click to expand

Conventional mutation testing tries to identify gaps in test coverage, whereas Necessist tries to identify bugs in existing tests.

Conventional mutation testing tools (such a universalmutator) randomly inject faults into source code, and see whether the code's tests still pass. If they do, it could mean the code's tests are inadequate.

Notably, conventional mutation testing is about finding deficiencies in the set of tests as a whole, not in individual tests. That is, for any given test, randomly injecting faults into the code is not especially likely to reveal bugs in that test. This is unfortunate since some tests are more important than others, e.g., because ensuring the correctness of some parts of the code is more important than others.

By comparison, Necessist's approach of iteratively removing statements and method calls does target individual tests, and thus can reveal bugs in individual tests.

Of course, there is overlap in the sets of problems the two approaches can uncover, e.g., a failure to find an injected fault could indicate a bug in a test. Nonetheless, for the reasons just given, we see the two approaches as complementary, not competing.

Possible theoretical foundation

Click to expand

The following criterion (*) comes close to describing the statements that Necessist aims to remove:

  • (*) Statement S's weakest precondition P has the same context (e.g., variables in scope) as S's postcondition Q, and P does not imply Q.

The notion that (*) tries to capture is: a statement that affects a subsequent assertion. In this section, we explain and motivate this choice. For concision, we focus on statements, but the remarks in this section apply to method calls as well.

Recall the two kinds of predicate transformer semantics: weakest precondition and strongest postcondition. With the former, one reasons about the weakest precondition that could hold prior to a statement, given a postcondition that holds after the statement. With the latter, one reasons about the strongest postcondition that could hold after a statement, given a precondition that holds prior to the statement. Generally speaking, the former is more common (see Aldrich 2013 for an explanation), and it is the one we use here.

Consider a test through this lens. A test is a function with no inputs or outputs. Thus, an alternative procedure for determining whether a test passes is the following. Starting with True, iteratively work backwards through the test's statements, computing the weakest precondition of each. If the precondition arrived at for the test's first statement is True, then the test passes. If the precondition is False, the test fails.

Now, imagine we were to apply this procedure, and consider a statement S that violates (*). We argue that it might not make sense to remove S:

Case 1: S adds or removes variables from the scope (e.g., S is a declaration), or S changes a variable's type. Then removing S would likely result in a compilation failure. (On top of that, since S's precondition and postcondition have different contexts, it's not clear how to compare them.)

Case 2: S's precondition is stronger than its postcondition (e.g., S is an assertion). Then S imposes constraints on the environments in which it executes. Put another way, S tests something. Thus, removing S would likely detract from the test's overarching purpose.

Conversely, consider a statement S that satisfies (*). Here is why it might make sense to remove S. Think of S as shifting the set of valid environments, rather than constraining them. More precisely, if S's weakest precondition P does not imply Q, and if Q is satisfiable, the there is an assignment to P and Q's free variables that satisfies both P and Q. If such an assignment results from each environment in which S is actually executed, then the necessity of S is called into question.

The main utility of (*) is in helping to select the functions, macros, and method calls that Necessist ignores. Necessist ignores certain of these by default. Suppose that, for one of the frameworks, we are considering whether Necessist should ignore some function foo. If we imagine a predicate transformer semantics for the framework's testing language, we can ask: if statement S were a call to foo, would S satisfy (*)? If the answer is "no," then Necessist should likely ignore foo.

Consider Rust's clone method, for example. A call to clone can be unnecessary. However, if we imagine a predicate transformer semantics for Rust, a call to clone is unlikely to satisfy (*). For this reason, Necessist does not attempt to remove clone calls.

In addition to helping to select the functions, etc. that Necessist ignores, (*) has other nice consequences. For example, the rule that the last statement in a test should be ignored follows from (*). To see this, note that such a statement's postcondition Q is always True. Thus, if the statement doesn't change the context, then its weakest precondition necessarily implies Q.

Having said all this, (*) doesn't quite capture what Necessist actually does. Consider a statement like x -= 1;. Necessist will remove such a statement unconditionally, but (*) says maybe Necessist shouldn't. Assuming overflow checks are enabled, computing this statement's weakest precondition would look something like the following:

{ Q[(x - 1)/x] ^ x >= 1 }
x -= 1;
{ Q }

Note that x -= 1; does not change the context, and that Q[(x - 1)/x] ^ x >= 1 could imply Q. For example, if Q does not contain x, then Q[(x - 1)/x] = Q and Q ^ x >= 1 implies Q.

Given the discrepancy between (*) and Necessist's current behavior, one can ask: which of the two should be adjusted? Put another way, should Necessist remove a statement like x -= 1; unconditionally?

One way to look at this question is: which statements are worth removing, i.e., which statements are "interesting?" As implied above, (*) considers a statement "interesting" if its removal could affect a subsequent assertion. But there are other possible, useful definitions of an "interesting" statement. For example, one could consider strongest postconditions (mentioned above), or frameworks besides Hoare logic entirely.

To be clear, Necessist does not apply (*) formally, e.g., Necessist does not actually compute weakest preconditions. The current role of (*) is to help guide which statements Necessist should ignore, and (*) seems to do well in that role. As such, we leave resolving the aforementioned discrepancy to future work.

Usage

Usage: necessist [OPTIONS] [TEST_FILES]... [-- <ARGS>...]

Arguments:
  [TEST_FILES]...  Test files to mutilate (optional)
  [ARGS]...        Additional arguments to pass to each test command

Options:
      --allow <WARNING>        Silence <WARNING>; `--allow all` silences all warnings
      --default-config         Create a default necessist.toml file in the project's root directory
      --deny <WARNING>         Treat <WARNING> as an error; `--deny all` treats all warnings as errors
      --dump                   Dump sqlite database contents to the console
      --dump-candidates        Dump removal candidates and exit (for debugging)
      --framework <FRAMEWORK>  Assume testing framework is <FRAMEWORK> [possible values: anchor-ts, auto, foundry, go, hardhat-ts, rust]
      --no-dry-run             Do not perform dry runs
      --no-sqlite              Do not output to an sqlite database
      --quiet                  Do not output to the console
      --reset                  Discard sqlite database contents
      --resume                 Resume from the sqlite database
      --root <ROOT>            Root directory of the project under test
      --timeout <TIMEOUT>      Maximum number of seconds to run any test; 60 is the default, 0 means no timeout
      --verbose                Show test outcomes besides `passed`
  -h, --help                   Print help
  -V, --version                Print version

Output

By default, Necessist outputs to the console only when tests pass. Passing --verbose causes Necessist to instead output all of the removal outcomes below.

Outcome Meaning (With the statement/method call removed...)
passed The test(s) built and passed.
timed-out The test(s) built but timed-out.
failed The test(s) built but failed.
nonbuildable The test(s) did not build.

By default, Necessist outputs to both the console and to an sqlite database. For the latter, a tool like sqlitebrowser can be used to filter/sort the results.

Details

Generally speaking, Necessist will not attempt to remove a statement if it is one the following:

  • a statement containing other statements (e.g., a for loop)
  • a declaration (e.g., a local or let binding)
  • a break, continue, or return
  • the last statement in a test

Similarly, Necessist will not attempt to remove a method call if:

  • It is the primary effect of an enclosing statement (e.g., x.foo();).
  • It appears in the argument list of an ignored function, method, or macro (see below).

Also, for some frameworks, certain statements and methods are ignored. Click on a framework to see its specifics.

Anchor TS

Ignored functions

  • assert
  • Anything beginning with assert. (e.g., assert.equal)
  • Anything beginning with console. (e.g., console.log)
  • expect

Ignored methods

  • toNumber
  • toString
Foundry

In addition to the below, the Foundry framework ignores:

  • a statement immediately following a use of vm.prank or any form of vm.expect (e.g., vm.expectRevert)
  • an emit statement

Ignored functions

  • Anything beginning with assert (e.g., assertEq)
  • Anything beginning with vm.expect (e.g., vm.expectCall)
  • Anything beginning with console.log (e.g., console.log, console.logInt)
  • Anything beginning with console2.log (e.g., console2.log, console2.logInt)
  • vm.getLabel
  • vm.label
Go

In addition to the below, the Go framework ignores:

  • defer statements

Ignored functions

  • Anything beginning with assert. (e.g., assert.Equal)
  • Anything beginning with require. (e.g., require.Equal)
  • panic

Ignored methods*

  • Close
  • Error
  • Errorf
  • Fail
  • FailNow
  • Fatal
  • Fatalf
  • Log
  • Logf
  • Parallel
  • Skip
  • Skipf
  • SkipNow

* This list is based primarily on testing.T's methods. However, some methods with commonplace names are omitted to avoid colliding with other types' methods.

Hardhat TS

The ignored functions and methods are the same as for Anchor TS above.

Rust

Ignored macros

  • assert
  • assert_eq
  • assert_matches
  • assert_ne
  • eprint
  • eprintln
  • panic
  • print
  • println
  • unimplemented
  • unreachable

Ignored methods*

  • as_bytes
  • as_encoded_bytes
  • as_mut
  • as_mut_os_str
  • as_mut_os_string
  • as_mut_slice
  • as_mut_str
  • as_os_str
  • as_path
  • as_ref
  • as_slice
  • as_str
  • borrow
  • borrow_mut
  • clone
  • cloned
  • copied
  • deref
  • deref_mut
  • expect
  • expect_err
  • into_boxed_bytes
  • into_boxed_os_str
  • into_boxed_path
  • into_boxed_slice
  • into_boxed_str
  • into_bytes
  • into_encoded_bytes
  • into_os_string
  • into_owned
  • into_path_buf
  • into_string
  • into_vec
  • iter
  • iter_mut
  • success
  • to_os_string
  • to_owned
  • to_path_buf
  • to_string
  • to_vec
  • unwrap
  • unwrap_err

* This list is essentially the watched trait and inherent methods of Dylint's unnecessary_conversion_for_trait lint, with the following additions:

Configuration files

A configuration file allows one to tailor Necessist's behavior with respect to a project. The file must be named necessist.toml, appear in the project's root directory, and be toml encoded. The file may contain one more of the options listed below.

  • ignored_functions, ignored_methods, ignored_macros: A list of strings interpreted as patterns. A function, method, or macro (respectively) whose path matches a pattern in the list is ignored. Note that ignored_macros is used only by the Rust framework currently.

  • ignored_path_disambiguation: One of the strings Either, Function, or Method. For a path that could refer to a function or method (see below), this option influences whether the function or method is ignored.

    • Either (default): Ignore if the path matches either an ignored_functions or ignored_methods pattern.

    • Function: Ignore only if the path matches an ignored_functions pattern.

    • Method: Ignore only if the path matches an ignored_methods pattern.

  • ignored_tests: A list of strings. A test whose name exactly matches a string in the list is ignored. For Mocha-based frameworks (e.g., Anchor and Hardhat), a test name is consider to be a message passed to it.

Patterns

A pattern is a string composed of letters, numbers, ., _, or *. Each character, other than *, is treated literally and matches itself only. A * matches any string, including the empty string.

The following are examples of patterns:

  • assert: matches itself only
  • assert_eq: matches itself only
  • assertEqual: matches itself only
  • assert.Equal: matches itself only
  • assert.*: matches assert.Equal, but not assert, assert_eq, or assertEqual
  • assert*: matches assert, assert_eq, assertEqual, and assert.Equal
  • *.Equal: matches assert.Equal, but not Equal

Notes:

  • Patterns match paths, not individual identifiers.
  • . is treated literally like in a glob pattern, not like in regular expression.

Paths

A path is a sequence of identifiers separated by .. Consider this example (from Chainlink):

operator.connect(roles.oracleNode).signer.sendTransaction({
    to: operator.address,
    data,
}),

In the above, operator.connect and signer.sendTransaction are paths.

Note, however, that paths like operator.connect are ambiguous:

  • If operator refers to package or module, then operator.connect refers to a function.
  • If operator refers to an object, then operator.connect refers to a method.

By default, Necessist ignores such a path if it matches either an ignored_functions or ignored_methods pattern. Setting the ignored_path_disambiguation option above to Function or Method causes Necessist ignore the path only if it matches an ignored_functions or ignored_methods pattern (respectively).

Limitations

  • Slow. Modifying tests requires them to be rebuilt. Running Necessist on even moderately sized codebases can take several hours.

  • Triage requires intimate knowledge of the source code. Generally speaking, Necessist does not produce "obvious" bugs. In our experience, deciding whether a statement/method call should be necessary requires intimate knowledge of the code under test. Necessist is best run on codebases for which one has (or intends to have) such knowledge.

Semantic versioning policy

We reserve the right to change what syntax Necessist ignores by default, and to consider such changes non-breaking.

Goals

  • If a project uses a supported framework, then cding into the project's directory and typing necessist (with no arguments) should produce meaningful output.

Anti-goals

  • Become a general-purpose mutation testing tool. Good such tools already exist (e.g., universalmutator).

References

  • Groce, A., Ahmed, I., Jensen, C., McKenney, P.E., Holmes, J.: How verified (or tested) is my code? Falsification-driven verification and testing. Autom. Softw. Eng. 25, 917โ€“960 (2018). A preprint is available. See Section 2.3.

License

Necessist is licensed and distributed under the AGPLv3 license. Contact us if you're looking for an exception to the terms.

necessist's People

Contributors

0xphaze avatar dependabot[bot] avatar disconnect3d avatar github-merge-queue[bot] avatar moodmosaic avatar smoelius avatar tarunbhm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

necessist's Issues

Don't remove method calls in ignored statements

This happens a lot with Go programs.

Example:

			t.Errorf("WriteT.%v: have err == nil, want non-nil", tv.Field(i).Type())
                        //                                                              ^^^^^^^

t.Errorf is ignored, but Necessist still tries to remove .Type().

This change probably should not be made until after the backends have been consolidated in the spirit of #247.

removal of closures and functions that contain `assert` macros cause false positives

Necessist will warn that a test is passing with code remove for the following examples:

  • A closure that contains an assertion
fn test1() {
std::thread::.spawn(move || {
                       ...
                        assert!(
                          ...
                        );
}

  • A function call that contains an assertion
fn test2() {
   let x = ...
   let y = ...
    my_assertion_helper(x, y);
}
fn my_assertion_helper(x, y) { assert!(...); }

Both of these mutations do not indicate that a test statement is unnecessary, and I think they should be ignored since assert is ignored.

Number of test files is misleading

Necessist might say 0 candidates in 0 test files when, in reality, test files were actually scanned.

The bug is that Necessist requires a test file to contain at least one candidate for the file to be counted in the total.

This is misleading and should be corrected.

Should error when `--dump` is passed and no necessist.db is found

What I imagine is that somehow, the arguments to this function are reworked, and a check is added involving the dump flag:

pub(crate) fn init(
root: &Path,
must_not_exist: bool,
reset: bool,
) -> Result<(Sqlite, Vec<crate::Removal>)> {

Here is where sqlite::init is called (you can see the dump flag is already incorporated into the must_not_exist argument):

necessist/core/src/core.rs

Lines 175 to 179 in 7cdeac5

let (sqlite, mut past_removals) = sqlite::init(
context.root,
!context.opts.dump && !context.opts.reset && !context.opts.resume,
context.opts.reset,
)?;

Ideally, the fix would also include a trycmd test to verify the new behavior. That would involve adding a test to this directory: https://github.com/trailofbits/necessist/tree/master/core/tests/necessist_db_absent

Reduce removal of nonbuildable statements

I see a few examples of where the exception list could be refined, however some might require some static analysis.
We could include exceptions for .function in vm.function and abi.function. With further analysis .Struct for Interface.Struct shouldn't be removed as well as .function in contract.function.

test/Test.t.sol:4457:11-4457:45: `.assume(_relayer > address(0x09));` nonbuildable
test/Test.t.sol:4458:9-4458:40: `vm.assume(_oracle != _relayer);` passed
test/Test.t.sol:4458:11-4458:40: `.assume(_oracle != _relayer);` nonbuildable
test/Test.t.sol:4461:9-4461:32: `vm.roll(startingBlock);` failed
test/Test.t.sol:4461:11-4461:32: `.roll(startingBlock);` nonbuildable
test/Test.t.sol:4466:35-4466:105: `.ProtocolFeeSettings(false, payable(address(0)), true, address(0), 0);` nonbuildable
test/Test.t.sol:4467:13-4467:91: `endpoint.updateProtocolFeeSettings("TestLibrary", 1, abi.encode(feeSettings));` passed
test/Test.t.sol:4467:21-4467:91: `.updateProtocolFeeSettings("TestLibrary", 1, abi.encode(feeSettings));` nonbuildable
test/Test.t.sol:4467:69-4467:89: `.encode(feeSettings)` nonbuildable
test/Test.t.sol:4470:46-4470:98: `.encode(false, address(_oracle), address(_relayer));` nonbuildable
test/Test.t.sol:4472:20-4472:121: `.encode(10, 10, 10, 10, _oracle, _relayer, address(this), address(0), address(0), true, true, false);` nonbuildable
test/Test.t.sol:4482:38-4482:49: `.encode(i);` nonbuildable
test/Test.t.sol:4483:45-4483:56: `.encode(i);` nonbuildable
test/Test.t.sol:4484:54-4484:71: `.encode(_relayer)` nonbuildable

Add option to "dry run" tests by just building them

This would benefit projects where (1) there are tests that are slow to run, and (2) those tests have no removal candidates.

Syntax-wise, the option might be--dry-run MODE, with MODE one of off, build-only, or on.

--no-dry-run would then become an alias for --dry-run off.

Get rid of `REQUIRE_NODE_MODULES`

However, for the long term, I am wondering if we can remove the REQUIRE_NODE_MODULES and simply rely on the presence of the package.json to decide if we should install node packages or not. There should be only rare cases in which the package.json exists and is not required to build or test the project.

Originally posted by @tarunbhm in #580 (comment)

Add a test to check whether third-party revisions are current

Contribution guidelines

Add contribution guidelines with the following:

  1. Describe different test suits and how to run them
  2. General and CI tests that are required to be run to ensure correct formatting, linting, and other things
  3. Documentation requirements if any

Better highlights for mutated code

Here is an example output on a Rust code base.

engine/src/multisig/client/keygen/tests.rs:72:2-72:28: `ceremony.complete().await;` passed
engine/src/multisig/client/keygen/tests.rs:105:2-105:28: `ceremony.complete().await;` passed

If statements become more complex and contain larger chains, it can become unclear which part has actually been removed. This could be shown in another color in the terminal output for example.

Improvements in foundry support

In this issue we list the improvements that can be made to the foundry support:

  1. Do not remove .expectCall lines in mutations
  2. Do not remove .log lines in mutations

Can I run necessist with nextest?

What a great tool ๐Ÿ‘ ๐Ÿ’ฏ

When using necessist in Rust, is it agnostic when it comes to the underlying test runner? I'd be interested to run this on a project where tests are run via cargo nextest run.

Incremental Testing based on `git-diff`

Hello @smoelius, @moodmosaic told me about this and found it quite intriguing and a great complement to the concept of mutants!

Currently, I'm focusing on incorporating mutation testing into Stacks. This led me to ponder how we could integrate this into our entire workflow. Similar to our approach with mutants, I see two primary applications for this in a large-scale project:

  1. Ensuring that all new tests are thoroughly vetted before their integration into the production environment.
  2. Identifying and improving existing tests that aren't up to the mark.

Given the extensive size of the stacks repository, we've created a specific process that runs with each pull request (PR) to indicate if the functions are inadequately tested, utilizing cargo-mutants. It's noteworthy that cargo-mutants already includes certain custom features for incremental runs, such as testing only those mutants associated with functions modified in a git-diff.

I'm curious if a similar approach could be applied to necessist, performing the operations only on modified tests based on a git-diff.

I'd greatly appreciate your thoughts on this and any feedback you might have. Also, thanks for your contributions to the field!

`--framework` should be more than just a suggestion

Currently, --framework can be used to resolve ambiguities when multiple frameworks apply.

But Necessist will still error if it believes the specified framework does not apply.

Necessist should consider --framework a command, and should error only if it cannot be honored.

Dry run all the test files together before initiating mutations

Currently Necessist dry runs each test file and then starts mutating it line by line to find surviving mutations. The dry run of the next file required an additional build after reverting the last mutation of the earlier file. We can try to improve performance by dry running all the files together at the start, which will allow us to skip the build of the original code again and again with a dry run of every test file.

However, there is a challenge with this approach. The Necessist is built with two goals:

  1. Provide useful information when something fails.
  2. Be able to keep going when something fails.

Therefore to be able to find which test file had a failing test case will require parsing logic for test output for all supported frameworks. We are keeping this issue for future if we can find a better approach to handle this case.

Check if package.json exists while installing node modules

The install_node_modules function does not check if the package.json file exists in the project directory. This function is used for foundry projects as well to support foundry projects with hardhat dependencies. But dry run fails for the pure foundry projects because they don't have a package.json file.

pub fn install_node_modules(context: &LightContext) -> Result<()> {
if context.root.join("node_modules").try_exists()? {
return Ok(());
}
// smoelius: If a `yarn.lock` file exists, use `yarn`. Otherwise, default to `npm install`.
let mut command = if context.root.join("yarn.lock").try_exists()? {
Command::new("yarn")
} else {
let mut command = Command::new("npm");
command.arg("install");
command
};
command.current_dir(context.root.as_path());
debug!("{:?}", command);
let output = command.output()?;
ensure!(output.status.success(), "{:#?}", output);
Ok(())
}

Adding a check if the package.json file exists will resolve this issue.

Use coverage?

It has been suggested a few times that Necessist could incorporate coverage information. Specifically, Necessist could forgo removing statements and method calls that are never executed. This would reduce Necessist's false positives, i.e., statements and method calls that Necessist incorrectly flags as unnecessary.

Thus far, I have avoided doing this because it could also introduce false negatives.

Specifically, if a statement is not included in Necessist's output, a user might conclude that the statement is necessary. In reality, the statement might just be dead.

Put another way, if the user reviews their coverage reports before running Necessist, then there is no problem. But it seems unwise to assume a user has reviewed their coverage reports.

Having said this, we might offer an option to use coverage. E.g., a --use-coverage option could say, "I understand the implications; go ahead an use coverage."

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.