#!/usr/bin/env perl
#-*- perl -*-
#
#  Copyright (C) 2002,2003 Ken'ichi Fukamachi
#   All rights reserved. This program is free software; you can
#   redistribute it and/or modify it under the same terms as Perl itself.
#
# $FML: find_bad_style.pl,v 1.8 2004/01/04 13:37:14 fukachan Exp $
#

use strict;
use Carp;
use File::stat;

my $in_sub    = 0;
my $in_head   = 0;
my $defined   = 0;
my $ioadapter = 0; 
my $close     = 0;
my $count     = 0;
my $copyright = '';
my $buf       = '';
my $prev_argv = '';
my $prev_line = '';
my $comment   = '';
my $reason    = '';
my $fnf_args  = '';
my $cur_fn    = '';
my $implicit  = ();
my $cur_buf   = '';

while (<>) {
    $cur_buf = $_;

    if (/^\#.*(Copyright.*)/i) {
	$copyright = $1;
    }

    if (/^\#.*\$FML:.*(\d{4})\/\d{2}\/\d{2} /i) {
	my $year = $1;
	print "\n$ARGV\n\twrong copyright\n" unless $copyright =~ /$year/;

	# compare mtime
	if (0) {
	    my $st = stat($ARGV);
	    my $mt = $st->mtime;
	    my ($sec,$min,$hour,$mday,$mon,$year,$wday) = localtime($mt);
	    $year += 1900;
	    print "\n$ARGV\n\twrong copyright\n" unless $copyright =~ /$year/;
	}
    }

    # reset the line number counter
    if ($prev_argv ne $ARGV) {
	_info($prev_argv);
	$count     = 0;
	$prev_argv = $ARGV;
    }
    $count++;

    # holds comment buf;
    $comment .= $_ if /^\#/;

    if (/^\s*\#/ || /^\s*$/) {
	$prev_line = $_;
	next;
    }

    # ignore documents
    if (/^\=\w+/) {
	$prev_line = $_;
	$in_head   = 1;
	$in_head   = 0 if /^\=cut/;
	next;
    }
    if ($in_head) {
	$prev_line = $_;
	next;
    }

    # 
    # 1. check the usage of open() and close() under not check of defined()
    # 
    if (/IO::Adapter/) { $ioadapter = 1;};
    if (/defined/) { $defined = 1;}
    if (/\$\S+\-\>(close|open)\(/ && (!/^sub /) && (!/^=head/) && (!/\$self/)) {
	unless ($defined || $ioadapter) {
	    $buf .= " ===> ". $_;
	    $buf =~ s/\n/\n\t/gm;
	    $buf =~ s/^/\t/;
	    print "\n$ARGV $count {\n\tdefined() ?\n\n", $buf, "\n}\n\n";
	}
    }


    # 
    # 2. check FNF
    # 
    if (/^sub /) {
	unless ( _is_fnf($comment) ) {
	    print "\n$ARGV $count\n   $_\twrong FNF ($reason)\n";
	}
	undef $comment;
    }

    if ($in_sub && /^    my.*\@_;\s*$/) {
	_check_args( $ARGV, $_ );
    }
    $fnf_args = '' if /^\}/;
    $fnf_args = '' if /^sub .*\}/;


    # 
    # 3. usage of $_ is wrong.
    # 
    if ($in_sub) {
	local($_) = $cur_buf;
	$_ =~ s@/[\w/]+/@//@g;

	if (/\$_/o && (! /\$_[a-zA-Z0-9]\w+/)) {
	    _log('use_underbar', $ARGV, $cur_buf);
	}
	if (/if\s+.*\/|if.*m\W/o && (! /\$.*[=!]\~/o) && /\/\S+\//o) {
	    _log('use_underbar', $ARGV, $cur_buf);
	}

	if (/if\(/) {
	    _log('if_style', $ARGV, $cur_buf);
	}

	if (/for\(/) {
	    _log('for_style', $ARGV, $cur_buf);
	}
    }


    # 
    # last resort: logging buffer
    # 
    if (/^sub (\S+)|^\}/) {
	undef $buf;
	$cur_fn    = $1 . "()";
	$in_sub    = 1;
	$defined   = 0;
	$ioadapter = 0;
    }
    if (/^sub .*\}/ || /^\}/o) {
	$in_sub = 0;
    }

    if ($in_sub) {
	$buf .= $_;
    }

    $prev_line = $_;
}

exit 0;


# Descriptions: check if $buf matches FNF style.
#    Arguments: STR($buf)
# Side Effects: update $reason
# Return Value: NUM
sub _is_fnf
{
    my ($buf) = @_;
    my $found = 0;
    my $is_arg = 0;
    my %type  = (
		 "Descriptions" => 0, 
		 "Arguments"    => 0, 
		 "Side Effects" => 0, 
		 "Return Value" => 0, 
		 );
    undef $reason;

    # special trap
    if ($buf =~ /\#\s+Descriptions:\s*\n\#\s+\S+/m) {
	$found |= 1;
	$type{ "Descriptions" } = 1;
    }

    for (split(/\n/, $buf)) {
	if (/\#\s+Descriptions:\s+\S+/o) { 
	    $found |= 1;
	    $type{ "Descriptions" } = 1;
	    $is_arg = 0;
	}
	if (/\#\s+Arguments:\s+\S+/o) { 
	    $found |= (1 << 1);
	    $type{ "Arguments" } = 1;
	    $is_arg = 1;
	}
	if (/\#\s+Side Effects:\s+\S+/o) { 
	    $found |= (1 << 2);
	    $type{ "Side Effects" } = 1;
	    $is_arg = 0;
	}
	if (/\#\s+Return Value:\s+\S+/o) { 
	    $found |= (1 << 3);
	    $type{ "Return Value" } = 1;
	    $is_arg = 0;
	}

	# hold argument buffer
	$fnf_args .= $_ if $is_arg;
    }

    if ($found == 15) {
	return 1;
    }
    else {
	for my $key (keys %type) {
	    unless ($type{ $key }) {
		$reason .= $reason ? ",$key" : $key;
	    }
	}

	if ($found == 0) {
	    $reason = "no comment lines";
	}

	return 0;
    }
}


# Descriptions: check consistency between @_ and FNF Arguments.
#    Arguments: STR($prog) STR($buf)
# Side Effects: none
# Return Value: none
sub _check_args
{
    my ($prog, $buf) = @_;
    my $fnf = $fnf_args;

    $buf =~ s/^\s*//;
    $buf =~ s/\s*$//;

    # my ( ... ) = @_;
    unless ($buf =~ /^my\s*\(/) {
	print "\n$prog\n   $cur_fn\n\tmissing my()? $buf\n\n";
	return;
    }

    $buf =~ s/my\s*\(//;
    $buf =~ s/\)\s*=.*$//;

    my (@args) = split(/\s*,\s*/, $buf);

    # FNF
    my (@fnf) = ();
    $fnf =~ s/\#//g;
    $fnf =~ s/\n/    /g;

    for (split(/\s+/, $fnf)) {
	s/\[//g;
	s/\]//g;
	if (/^\w+\((\S+)\)/) {
	    push(@fnf, $1);
	}
    }

    my $bad = 0;
    if ($#args != $#fnf) {
	$bad = 1;
    }
    else {
	for (my $i = 0; $i <= $#args; $i++) {
	    if ($args[ $i ] ne $fnf[ $i ]) {
		$bad = 1;
	    }
	}
    }

    if ($bad) {
	print "\n$prog\n   $cur_fn\n\twrong arguments\n";
	print "\tFNF  @fnf\n";
	print "\t\@_   @args\n";
	print "\n";
    }
}


# Descriptions: set hash at %implicit
#    Arguments: STR($key) STR($file) STR($buf)
# Side Effects: update %implicit
# Return Value: none
sub _log
{
    my ($key, $file, $buf) = @_;

    $buf =~ s/^\s*//;
    $implicit->{ $key }->{ $file } .= "\t ($count)> ".$buf;
}


# Descriptions: show value of %implicit for the specified $file.
#    Arguments: STR($file)
# Side Effects: none
# Return Value: none
sub _info
{
    my ($file) = @_;
    my %type = (
		'use_underbar' => 'use of $_',
		'if_style'     => 'wrong style: if',
		'for_style'    => 'wrong style: for',
		);

    for my $key (sort keys %$implicit) {
	if ($implicit->{ $key }->{ $file }) {
	    print "\n$file\n";
	    print "\t", $type{ $key } ,"\n";
	    print $implicit->{ $key }->{ $file };
	}
    }
}
