# python::python::helpers -- lintian check script -*- perl -*-
#
# Copyright © 2011, 2012, 2013 Jakub Wilk
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, you can find it on the World Wide
# Web at http://www.gnu.org/copyleft/gpl.html, or write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
# MA 02110-1301, USA.

package Lintian::python::helpers;

use strict;
use warnings;

use Lintian::Tags qw(tag);
use Lintian::Util qw(fail);

use List::MoreUtils qw(each_array);
use Text::Levenshtein qw();

# FIXME: copied from checks/files
my @METAPKG_REGEX =
    (qr/meta[ -]?package/, qr/dummy/,
     qr/(?:dependency|empty|transitional|virtual) package/);

sub run {

    my ($pkg, $type, $info) = @_;
    if ($type eq 'source') {
        return run_source(@_);
    } else {
        return run_binary(@_);
    }

}

sub is_meta_pkg {
    # FIXME: this duplicates checks/files logic
    my ($info, $binpkg) = @_;
    my $description = $info->binary_field($binpkg, 'description') // '';
    for my $regex (@METAPKG_REGEX) {
        if ($description =~ /$regex/) {
            return 1;
        }
    }
    return 0;
}

sub is_doc_pkg {
    my ($info, $binpkg) = @_;
    return ($info->binary_field($binpkg, 'section') // '') eq 'doc';
}

sub is_dev_pkg {
    my ($info, $binpkg) = @_;
    return $binpkg =~ m/-(dbg|dev)$/;
}

sub is_boring_pkg {
    my ($info, $binpkg) = @_;
    return
        is_doc_pkg($info, $binpkg) ||
        is_dev_pkg($info, $binpkg) ||
        is_meta_pkg($info, $binpkg);
}

sub run_source {

    my ($pkg, $type, $info) = @_;

    my $bdepends = $info->relation('build-depends-all');

    return if $pkg =~ m/^python(?:\d\.\d)?$/;
    return if $pkg =~ m/^python\d?-(?:stdlib-extensions|profiler|old-doctools)$/;
    return if $pkg =~ m/^python3?-defaults$/;
    return if $pkg =~ m/^python-(?:central|support)$/;

    my %python_depends = ();

    my @python_version_declarations = ();
    my $pysupport_bd = $bdepends->implies('python-support');
    if (defined (my $pv = $info->source_field('xs-python-version'))) {
        while ($pv =~ /\$\{[^}]++\}/g) {
            tag 'substitution-variable-in-xs-python-version', $pv;
        }
        if ($pv =~ /^current\b/) {
            tag 'xs-python-version-current-is-deprecated';
        }
        unless ($pysupport_bd) {
            tag 'xs-python-version-instead-of-x-python-version'
        }
        push @python_version_declarations, 'XS-Python-Version';
    }
    if ($info->field('python3-version')) {
        tag 'xs-python3-version-instead-of-x-python3-version';
    }
    if (defined(my $xpv = $info->source_field('x-python-version'))) {
        $xpv =~ s/^\s*|\s$//g;
        $xpv =~ s/\s+/ /g;
        my $vr = qr/2\.0 | 2\.[1-9]\d*/x;
        if ($xpv !~ m/^(?:>= ?$vr(?: ?, ?<< ?$vr)?|$vr|all)$/) {
            tag 'invalid-python-version-declaration', "X-Python-Version: $xpv";
        }
        push @python_version_declarations, 'X-Python-Version';
        if ($pysupport_bd) {
            tag 'x-python-version-instead-of-xs-python-version'
        }
    }
    if (defined(my $xpv = $info->source_field('x-python3-version'))) {
        $xpv =~ s/^\s*|\s$//g;
        $xpv =~ s/\s+/ /g;
        my $vr = qr/3\.0 | 3\.[1-9]\d*/x;
        if ($xpv !~ m/^(?:>= ?$vr(?: ?, ?<< ?$vr\d*)?|$vr|all)$/) {
            tag 'invalid-python-version-declaration', "X-Python3-Version: $xpv";
        }
    }
    if (-e $info->debfiles('pyversions')) {
        push @python_version_declarations, 'debian/pyversions';
    }
    if (scalar @python_version_declarations > 1) {
        tag 'multiple-python-version-declarations', "@python_version_declarations";
    }

    my @source_fields = grep { ! m/^xs?-python3?-version$/ } keys($info->source_field);
    my @distances = Text::Levenshtein::distance("x-python-version", @source_fields);
    my $iterator = each_array(@distances, @source_fields);
    while (my ($distance, $field) = $iterator->()) {
        if ($distance <= 3) {
            tag 'typo-in-python-version-declaration', $field, '->',
                ($pysupport_bd ? 'xs-python-version' : 'x-python-version');
        }
    }

    if (-e $info->debfiles('pycompat')) {
        tag 'debian-pycompat-is-obsolete';
    }

    my @binpkgs = sort $info->binaries;
    for my $binpkg (@binpkgs) {
        if ($info->binary_relation($binpkg, 'all')->implies('${python:Depends}')) {
            $python_depends{$binpkg} = 1;
            if ($binpkg =~ m/^python3-/) {
                tag 'python3-module-but-python-depends', $binpkg;
            }
        } else {
            if ($binpkg =~ /^python-/ and not is_boring_pkg($info, $binpkg)) {
                tag 'python-module-but-no-python-depends', $binpkg
            }
            for my $helper (qw(python-support python-central)) {
                if ($info->binary_relation($binpkg, 'all')->implies($helper)) {
                    tag 'hardcoded-dependency-on-python-helper', $helper;
                }
            }
        }
        if ($info->binary_relation($binpkg, 'all')->implies('${python3:Depends}')) {
            if ($binpkg =~ m/^python-/) {
                tag 'python-module-but-python3-depends', $binpkg;
            }
        } else {
            if ($binpkg =~ /^python3-/ and not is_boring_pkg($info, $binpkg)) {
                tag 'python3-module-but-no-python3-depends', $binpkg
            }
        }
        for my $python (qw(python python3)) {
            if ($info->binary_relation($binpkg, 'provides')->implies("\${$python:Provides}")) {
                tag 'python-provides-considered-harmful', $binpkg;
            }
            if ($info->binary_relation($binpkg, 'breaks')->implies("\${$python:Breaks}")) {
                tag 'python-breaks-is-obsolete', $binpkg;
            }
        }
    }

    my $rules = $info->debfiles('rules');
    if (-l $rules or not -f $rules) {
        return;
    }
    open(my $fh, '<', $rules) or fail("cannot open rules: $!");
    while (<$fh>) {
        while (s,\\$,, and defined (my $cont = <$fh>)) {
            $_ .= $cont;
        }
        if (m,(This file was automatically generated by stdeb(?: [0-9]\S+)?),i) {
            tag 'python-stdeb-boilerplate', "debian/rules:$.", "\"$1\"";
        }
        next if /^\s*\#/;
        if (keys %python_depends) {
            if (m,\bpython:Depends\b,) {
                # generated by hand
                %python_depends = ();
                last;
            } elsif (m,\bdh_(?:python2?|pysupport|pycentral)\b,) {
                # generated by explicit call to a Python helper
                %python_depends = ();
                last;
            } elsif (m,^include\s+/usr/share/cdbs/1/class/python-,m) {
                # generated by Python helper via CDBS
                %python_depends = ();
                last;
            } elsif (m,\bdh\b,) {
                # Lintian proper already takes care of this part.
                %python_depends = ();
                last;
            }
        }
    }
    close($fh);
    if (keys %python_depends) {
        tag 'python-depends-but-no-python-helper', keys %python_depends;
    }
}

sub run_binary {

    my ($pkg, $type, $info) = @_;
    my $pycentral_data_path = "usr/share/pyshared-data/$pkg";
    my $pycentral_data_stat = $info->index($pycentral_data_path);
    if (defined($pycentral_data_stat) and $pycentral_data_stat->{'type'} eq '-') {
        my %files = ();
        open(my $fh, '<', $info->unpacked($pycentral_data_path)) or fail("cannot open python-central metadata: $!");
        my $files_section = 0;
        while (<$fh>) {
            if (m,^\[files\]$,) {
                $files_section = 1;
                next;
            } elsif (m,^\[,) {
                $files_section = 0;
                next;
            } elsif ($files_section) {
                if (m,^/(.+)=([fdYN])$,) {
                    my ($filename, $type) = ($1, $2);
                    $files{$filename} = 1;
                } else {
                    tag 'cannot-parse-python-central-metadata', "line $.: $_";
                    last;
                }
            }
        }
        close($fh);
        while (my ($filename, $type) = each %files) {
            my $fileinfo =
                $info->index($filename) ||
                $info->index("$filename/");
            if (not defined $fileinfo) {
                tag 'python-central-metadata-for-missing-files', $filename;
            }
        }
    }

    if (defined $info->field('python-version')) {
        tag 'xb-python-version-is-deprecated'
            unless ($info->relation('all')->implies('python-central'));
    }

    unless (-l 'changelog' or not -f 'changelog') {
        check_changelog('changelog');
    }

}

sub check_changelog {
    my ($filename) = @_;
    open (my $fh, '<', $filename) or fail("cannot open $filename");
    my $entry_no = 0;
    while (<$fh>) {
        if (m,^\S,) {
            $entry_no += 1;
            return if $entry_no >= 2;
        }
        elsif (m,(source package automatically created by stdeb(?: [0-9]\S+)?),i,) {
            tag 'python-stdeb-boilerplate', "$filename:$.", "\"$1\"";
        }
    }
    close $fh;
}

1;

# Local Variables:
# indent-tabs-mode: nil
# cperl-indent-level: 4
# End:
# vim: syntax=perl sw=4 sts=4 sr et
