The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
#!/usr/bin/perl
package Devel::Assert;

use strict;
use Devel::Declare ();
use Devel::Assert::Parser;

my $FORCE_ASSERTS = undef;
my $ASSERT_STATUS = {};
my $ASSERT_CONDS = {};
my $cond_counter = 0;

$Carp::Internal{'Devel::Assert'}++;
$Carp::Internal{'Devel::Declare'}++;

our $VERSION = '0.02';

my $fail_actions = {
	verbose			=> 0,
	hook_terse		=> \&hook_terse,
	hook_verbose	=> \&hook_verbose,
};

sub import{
	my $class = shift;
	my $caller = caller;

	my $flag = shift;

	if ($flag && $flag eq '-verbose'){
		$fail_actions->{'verbose'} = 1;
		$flag = shift;
	}

	if (!defined($FORCE_ASSERTS) && $flag){
		if ($flag eq '-none'){
			$FORCE_ASSERTS = 0;
		}elsif($flag eq '-all'){
			$FORCE_ASSERTS = 1;
		}
	}

	if (defined($FORCE_ASSERTS)){
		$ASSERT_STATUS->{$caller} = $FORCE_ASSERTS;
	}else{
		$ASSERT_STATUS->{$caller} = $flag ? 1 : 0;
	}

	if (scalar @_ == 1 && $_[0] eq '-verbose'){
		$fail_actions->{'verbose'} = 1;
	}elsif(scalar @_){
		require Carp;
		Carp::croak('Wrong options supplied for Devel::Assert');
	}

	Devel::Declare->setup_for(
		$caller,
		{ assert => { const => \&parse_assert } }
	);

	no strict 'refs';
	if ($ASSERT_STATUS->{$caller}){
		*{$caller.'::assert'} = sub ($$) {warn "this shouldn't be called - report your case to author\n"};
	}else{
		*{$caller.'::assert'} = sub () { warn "this shouldn't be called - report your case to author\n" };
	}
}

sub unimport{
	$ASSERT_STATUS->{scalar caller} = 0;
}

sub set_options{
	my %params = @_;

	$fail_actions->{'verbose'} = $params{'verbose'}
		if exists $params{'verbose'};

	$fail_actions->{'hook_terse'} = $params{'hook_terse'}
		if exists $params{'hook_terse'} && ref($params{'hook_terse'}) eq 'CODE';

	$fail_actions->{'hook_verbose'} = $params{'hook_verbose'}
		if exists $params{'hook_verbose'} && ref($params{'hook_verbose'}) eq 'CODE';
}

sub parse_assert{
	my $parser = Devel::Assert::Parser->new($_[1]);

	return if $parser->get_word() ne 'assert';

	$parser->skip_word();
	$parser->skip_spaces();
	return if $parser->get_symbols(2) eq '=>';

	if ($parser->get_symbols(1) ne '('){
		require Carp;
		Carp::croak("You must use assert ONLY as 'assert(expression);'");
	}

	my $args = $parser->extract_args();
	$args =~ s/(\r|\n)//go;

	my $pkg = Devel::Declare::get_curstash_name;

	if (!$ASSERT_STATUS->{$pkg} || !length($args)){
		$parser->shadow(sub () { 1 });

	}else{
		$parser->inject("($args, $cond_counter)");

		$args =~ s/^\s+//;
		$args =~ s/\s+$//;
		$ASSERT_CONDS->{$cond_counter} = $args;

		$parser->shadow(sub ($$) {
			if (!$_[0]) {
				if ($fail_actions->{'verbose'}){
					return $fail_actions->{'hook_verbose'}->($ASSERT_CONDS->{$_[1]});
				}else{
					return $fail_actions->{'hook_terse'}->($ASSERT_CONDS->{$_[1]});
				}
			}
		});

		$cond_counter++;
	}
}

sub hook_terse{
	require Carp;
	Carp::confess("Assertion ' $_[0] ' failed$_[1]");
}

sub hook_verbose{
	my $message = shift;

	eval { require Data::Dumper; 1 } or do {
		warn "Asked for detailed variables report, but no 'Data::Dumper' found\n";
		return $fail_actions->{'hook_terse'}->($message);
	};

	eval { require PadWalker; 1 } or do {
		warn "Asked for detailed variables report, but no 'PadWalker' found\n";
		return $fail_actions->{'hook_terse'}->($message);
	};

	my $tail = ", trying to determine acting variables...";
	my @var_list = ();
	my @names_list = ();

	my $my_list = PadWalker::peek_my(2);
	while (my ($name, $val_ref) = each(%$my_list)) {
		$name =~ s/\$/\\\$/;
		if ($message =~ /$name(?:\W(?<!:)|$)/){
			if ($name =~ m'^\\\$'){
				push @var_list, $$val_ref;
				$name =~ s/\\\$/\$/;
			}else{
				push @var_list, $val_ref;
			}

			$name =~ s/[\$\@\%]/\*/;
			push @names_list, $name;
		}
	}

	my $our_list = PadWalker::peek_our(2);
	while (my ($name, $val_ref) = each(%$our_list)) {
		next if exists $my_list->{$name};

		$name =~ s/\$/\\\$/;
		if ($message =~ /$name(?:\W(?<!:)|$)/){
			if ($name =~ m'^\\\$'){
				push @var_list, $$val_ref;
				$name =~ s/\\\$/\$/;
			}else{
				push @var_list, $val_ref;
			}

			$name =~ s/[\$\@\%]/\*/;
			push @names_list, $name;
		}
	}

	if (scalar @var_list == 0){
		$tail .= "none found\n";
	}else{
		local $Data::Dumper::Maxdepth = 2;
		local $Data::Dumper::Pad = '  ';
		$tail .= "\n".Data::Dumper->Dump(\@var_list, \@names_list);
	}

	$tail .= "...and all this happened";
	return $fail_actions->{'hook_terse'}->($message, $tail);
}

1;