# debhelper format -- lintian check script -*- perl -*-

# Copyright (C) 1999 by Joey Hess
# Copyright (C) 2016-2020 Chris Lamb <lamby@debian.org>
# Copyright (C) 2021 Felix Lechner
#
# 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 https://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::Check::Debhelper;

use v5.20;
use warnings;
use utf8;

use Const::Fast;
use List::Compare;
use List::SomeUtils qw(any firstval);
use List::UtilsBy qw(min_by);
use Text::LevenshteinXS qw(distance);
use Unicode::UTF8 qw(encode_utf8);

use Lintian::Relation;

const my $EMPTY => q{};
const my $SPACE => q{ };
const my $DOLLAR => q{$};
const my $UNDERSCORE => q{_};
const my $HORIZONTAL_BAR => q{|};

const my $ARROW => q{=>};

# If there is no debian/compat file present but cdbs is being used, cdbs will
# create one automatically.  Currently it always uses compatibility level 5.
# It may be better to look at what version of cdbs the package depends on and
# from that derive the compatibility level....
const my $CDBS_COMPAT => 5;

# minimum versions for features
const my $BRACE_EXPANSION => 5;
const my $USES_EXECUTABLE_FILES => 9;
const my $DH_PARALLEL_NOT_NEEDED => 10;
const my $REQUIRES_AUTOTOOLS => 10;
const my $USES_AUTORECONF => 10;
const my $INVOKES_SYSTEMD => 10;
const my $BETTER_SYSTEMD_INTEGRATION => 11;
const my $VERSIONED_PREREQUISITE_AVAILABLE => 11;

const my $LEVENSHTEIN_TOLERANCE => 3;
const my $MANY_OVERRIDES => 20;

use Moo;
use namespace::clean;

with 'Lintian::Check';

my $MISC_DEPENDS = Lintian::Relation->new->load($DOLLAR . '{misc:Depends}');

# Manually maintained list of dh_commands that requires a versioned
# dependency *AND* are not provided by debhelper.  Commands provided
# by debhelper is handled in checks/debhelper.
#
# This overrules any thing listed in dh_commands (which is auto-generated).

my %DH_COMMAND_MANUAL_PREREQUISITES = (
    dh_apache2 => 'dh-apache2:any | apache2-dev:any',
    dh_autoreconf_clean =>
'dh-autoreconf:any | debhelper:any (>= 9.20160403~) | debhelper-compat:any',
    dh_autoreconf =>
'dh-autoreconf:any | debhelper:any (>= 9.20160403~) | debhelper-compat:any',
    dh_dkms => 'dh-dkms:any | dh-sequence-dkms:any',
    dh_girepository => 'gobject-introspection:any | dh-sequence-gir:any',
    dh_gnome => 'gnome-pkg-tools:any | dh-sequence-gnome:any',
    dh_gnome_clean => 'gnome-pkg-tools:any | dh-sequence-gnome:any',
    dh_lv2config => 'lv2core:any',
    dh_make_pgxs => 'postgresql-server-dev-all:any | postgresql-all:any',
    dh_nativejava => 'gcj-native-helper:any | default-jdk-builddep:any',
    dh_pgxs_test => 'postgresql-server-dev-all:any | postgresql-all:any',
    dh_python2 => 'dh-python:any | dh-sequence-python2:any',
    dh_python3 =>
      'dh-python:any | dh-sequence-python3:any | pybuild-plugin-pyproject:any',
    dh_sphinxdoc =>
'sphinx:any | python-sphinx:any | python3-sphinx:any | dh-sequence-sphinxdoc:any',
    dh_xine => 'libxine-dev:any | libxine2-dev:any'
);

# Manually maintained list of dependencies needed for dh addons. This overrides
# information from data/common/dh_addons (the latter file is automatically
# generated).
my %DH_ADDON_MANUAL_PREREQUISITES = (
    ada_library => 'dh-ada-library:any | dh-sequence-ada-library:any',
    apache2 => 'dh-apache2:any | apache2-dev:any',
    autoreconf =>
'dh-autoreconf:any | debhelper:any (>= 9.20160403~) | debhelper-compat:any',
    cli => 'cli-common-dev:any | dh-sequence-cli:any',
    dwz => 'debhelper:any | debhelper-compat:any | dh-sequence-dwz:any',
    installinitramfs =>
'debhelper:any | debhelper-compat:any | dh-sequence-installinitramfs:any',
    gir => 'gobject-introspection:any | dh-sequence-gir:any',
    gnome => 'gnome-pkg-tools:any | dh-sequence-gnome:any',
    lv2config => 'lv2core:any',
    nodejs => 'pkg-js-tools:any | dh-sequence-nodejs:any',
    perl_dbi => 'libdbi-perl:any | dh-sequence-perl-dbi:any',
    perl_imager => 'libimager-perl:any | dh-sequence-perl-imager:any',
    pgxs => 'postgresql-server-dev-all:any | postgresql-all:any',
    pgxs_loop => 'postgresql-server-dev-all:any | postgresql-all:any',
    pypy => 'dh-python:any | dh-sequence-pypy:any',
    python2 => 'python2:any | python2-dev:any | dh-sequence-python2:any',
    python3 =>
'python3:any | python3-all:any | python3-dev:any | python3-all-dev:any | dh-sequence-python3:any',
    scour => 'scour:any | python-scour:any | dh-sequence-scour:any',
    sphinxdoc =>
'sphinx:any | python-sphinx:any | python3-sphinx:any | dh-sequence-sphinxdoc:any',
    systemd =>
'debhelper:any (>= 9.20160709~) | debhelper-compat:any | dh-sequence-systemd:any | dh-systemd:any',
    vim_addon => 'dh-vim-addon:any | dh-sequence-vim-addon:any',
);

sub visit_patched_files {
    my ($self, $item) = @_;

    return
      unless $item->dirname eq 'debian/';

    return
      if !$item->is_symlink && !$item->is_file;

    if (   $item->basename eq 'control'
        || $item->basename =~ m/^(?:.*\.)?(?:copyright|changelog|NEWS)$/) {

        # Handle "control", [<pkg>.]copyright, [<pkg>.]changelog
        # and [<pkg>.]NEWS

        # The permissions of symlinks are not really defined, so resolve
        # $item to ensure we are not dealing with a symlink.
        my $actual = $item->resolve_path;

        $self->pointed_hint('package-file-is-executable', $item->pointer)
          if $actual && $actual->is_executable;

        return;
    }

    return;
}

sub source {
    my ($self) = @_;

    my @MAINT_COMMANDS = @{$self->data->debhelper_commands->maint_commands};

    my $FILENAME_CONFIGS= $self->data->load('debhelper/filename-config-files');

    my $DEBHELPER_LEVELS = $self->data->debhelper_levels;
    my $DH_ADDONS = $self->data->debhelper_addons;
    my $DH_COMMANDS_DEPENDS= $self->data->debhelper_commands;

    my @KNOWN_DH_COMMANDS;
    for my $command ($DH_COMMANDS_DEPENDS->all) {
        for my $focus ($EMPTY, qw(-arch -indep)) {
            for my $timing (qw(override execute_before execute_after)) {

                push(@KNOWN_DH_COMMANDS,
                    $timing . $UNDERSCORE . $command . $focus);
            }
        }
    }

    my $debhelper_level;
    my $dh_compat_variable;
    my $maybe_skipping;

    my $uses_debhelper = 0;
    my $uses_dh_exec = 0;
    my $uses_autotools_dev_dh = 0;

    my $includes_cdbs = 0;
    my $modifies_scripts = 0;

    my $seen_any_dh_command = 0;
    my $seen_dh_sequencer = 0;
    my $seen_dh_dynamic = 0;
    my $seen_dh_systemd = 0;
    my $seen_dh_parallel = 0;
    my $seen_dh_clean_k = 0;

    my %command_by_prerequisite;
    my %addon_by_prerequisite;
    my %overrides;

    my $droot = $self->processable->patched->resolve_path('debian/');

    my $drules;
    $drules = $droot->child('rules') if $droot;

    return
      unless $drules && $drules->is_open_ok;

    open(my $rules_fd, '<', $drules->unpacked_path)
      or die encode_utf8('Cannot open ' . $drules->unpacked_path);

    my $command_prefix_pattern = qr/\s+[@+-]?(?:\S+=\S+\s+)*/;

    my $build_prerequisites_norestriction
      = $self->processable->relation_norestriction('Build-Depends-All');
    my $build_prerequisites= $self->processable->relation('Build-Depends-All');

    my %seen = (
        'python2' => 0,
        'python3' => 0,
        'runit'   => 0,
        'sphinxdoc' => 0,
    );

    for (qw(python2 python3)) {

        $seen{$_} = 1
          if $build_prerequisites_norestriction->satisfies(
            "dh-sequence-$_:any");
    }

    my %build_systems;

    my $position = 1;
    while (my $line = <$rules_fd>) {

        my $pointer = $drules->pointer($position);

        while ($line =~ s/\\$// && defined(my $cont = <$rules_fd>)) {
            $line .= $cont;
        }

        if ($line =~ /^ifn?(?:eq|def)\s/) {
            $maybe_skipping++;

        } elsif ($line =~ /^endif\s/) {
            $maybe_skipping--;
        }

        next
          if $line =~ /^\s*\#/;

        if ($line =~ /^$command_prefix_pattern(dh_(?!autoreconf)\S+)/) {

            my $dh_command = $1;

            $build_systems{'debhelper'} = 1
              unless exists $build_systems{'dh'};

            $self->pointed_hint('dh_installmanpages-is-obsolete',$pointer)
              if $dh_command eq 'dh_installmanpages';

            if (   $dh_command eq 'dh_autotools-dev_restoreconfig'
                || $dh_command eq 'dh_autotools-dev_updateconfig') {

                $self->pointed_hint(
                    'debhelper-tools-from-autotools-dev-are-deprecated',
                    $pointer, $dh_command);
                $uses_autotools_dev_dh = 1;
            }

            # Record if we've seen specific helpers, special-casing
            # "dh_python" as Python 2.x.
            $seen{'python2'} = 1 if $dh_command eq 'dh_python2';
            for my $k (keys %seen) {
                $seen{$k} = 1 if $dh_command eq "dh_$k";
            }

            $seen_dh_clean_k = 1
              if $dh_command eq 'dh_clean'
              && $line =~ /\s+\-k(?:\s+.*)?$/s;

            # if command is passed -n, it does not modify the scripts
            $modifies_scripts = 1
              if (any { $dh_command eq $_ } @MAINT_COMMANDS)
              && $line !~ /\s+\-n\s+/;

           # If debhelper commands are wrapped in make conditionals, assume the
           # maintainer knows what they're doing and don't check build
           # dependencies.
            unless ($maybe_skipping) {

                if (exists $DH_COMMAND_MANUAL_PREREQUISITES{$dh_command}) {
                    my $prerequisite
                      = $DH_COMMAND_MANUAL_PREREQUISITES{$dh_command};
                    $command_by_prerequisite{$prerequisite} = $dh_command;

                } elsif ($DH_COMMANDS_DEPENDS->installed_by($dh_command)) {

                    my @broadened = map { "$_:any" }
                      $DH_COMMANDS_DEPENDS->installed_by($dh_command);
                    my $prerequisite
                      = join($SPACE . $HORIZONTAL_BAR . $SPACE,@broadened);
                    $command_by_prerequisite{$prerequisite} = $dh_command;
                }
            }

            $seen_any_dh_command = 1;
            $uses_debhelper = 1;

        } elsif ($line =~ m{^(?:$command_prefix_pattern)dh\s+}) {

            $build_systems{'dh'} = 1;
            delete($build_systems{'debhelper'});

            $seen_dh_sequencer = 1;
            $seen_any_dh_command = 1;

            $seen_dh_dynamic = 1
              if $line =~ /\$[({]\w/;

            $seen_dh_parallel = $position
              if $line =~ /--parallel/;

            $uses_debhelper = 1;
            $modifies_scripts = 1;

            while ($line =~ /\s--with(?:=|\s+)(['"]?)(\S+)\1/g) {

                my $addon_list = $2;

                for my $addon (split(/,/, $addon_list)) {

                    my $orig_addon = $addon;

                    $addon =~ y,-,_,;

                    my @broadened
                      = map { "$_:any" } $DH_ADDONS->installed_by($addon);
                    my $prerequisite = $DH_ADDON_MANUAL_PREREQUISITES{$addon}
                      || join($SPACE . $HORIZONTAL_BAR . $SPACE,@broadened);

                    if ($addon eq 'autotools_dev') {

                        $self->pointed_hint(
'debhelper-tools-from-autotools-dev-are-deprecated',
                            $pointer,"dh ... --with $orig_addon"
                        );
                        $uses_autotools_dev_dh = 1;
                    }

                    $seen_dh_systemd = $position
                      if $addon eq 'systemd';

                    $self->pointed_hint(
                        'dh-quilt-addon-but-quilt-source-format',
                        $pointer,"dh ... --with $orig_addon")
                      if $addon eq 'quilt'
                      && $self->processable->fields->value('Format') eq
                      '3.0 (quilt)';

                    $addon_by_prerequisite{$prerequisite} = $addon
                      if defined $prerequisite;

                    for my $k (keys %seen) {
                        $seen{$k} = 1
                          if $addon eq $k;
                    }
                }
            }

        } elsif ($line =~ m{^include\s+/usr/share/cdbs/1/rules/debhelper.mk}
            || $line =~ m{^include\s+/usr/share/R/debian/r-cran.mk}) {

            $build_systems{'cdbs-with-debhelper.mk'} = 1;
            delete($build_systems{'cdbs-without-debhelper.mk'});

            $seen_any_dh_command = 1;
            $uses_debhelper = 1;
            $modifies_scripts = 1;
            $includes_cdbs = 1;

            # CDBS sets DH_COMPAT but doesn't export it.
            $dh_compat_variable = $CDBS_COMPAT;

        } elsif ($line =~ /^\s*export\s+DH_COMPAT\s*:?=\s*([^\s]+)/) {
            $debhelper_level = $1;

        } elsif ($line =~ /^\s*export\s+DH_COMPAT/) {
            $debhelper_level = $dh_compat_variable
              if $dh_compat_variable;

        } elsif ($line =~ /^\s*DH_COMPAT\s*:?=\s*([^\s]+)/) {
            $dh_compat_variable = $1;

            # one can export and then set the value:
            $debhelper_level = $1
              if $debhelper_level;

        } elsif (
            $line =~ /^[^:]*(override|execute_(?:after|before))\s+(dh_[^:]*):/)
        {
            $self->pointed_hint('typo-in-debhelper-override-target',
                $pointer, "$1 $2",$ARROW, "$1_$2");

        } elsif ($line =~ /^([^:]*_dh_[^:]*):/) {

            my $alltargets = $1;
            # can be multiple targets per rule.
            my @targets = split(/\s+/, $alltargets);
            my @dh_targets = grep { /_dh_/ } @targets;

            # If maintainer is using wildcards, it's unlikely to be a typo.
            my @no_wildcards = grep { !/%/ } @dh_targets;

            my $lc = List::Compare->new(\@no_wildcards, \@KNOWN_DH_COMMANDS);
            my @unknown = $lc->get_Lonly;

            for my $target (@unknown) {

                my %distance
                  = map { $_ => distance($target, $_) } @KNOWN_DH_COMMANDS;
                my @near = grep { $distance{$_} < $LEVENSHTEIN_TOLERANCE }
                  keys %distance;
                my $nearest = min_by { $distance{$_} } @near;

                $self->pointed_hint('typo-in-debhelper-override-target',
                    $pointer, $target, $ARROW, $nearest)
                  if length $nearest;
            }

            for my $target (@no_wildcards) {

                next
                  unless $target
                  =~ /^(override|execute_(?:before|after))_dh_([^\s]+?)(-arch|-indep|)$/;

                my $timing = $1;
                my $command = $2;
                my $focus = $3;
                my $dh_command = "dh_$command";

                $overrides{$dh_command} = [$position, $focus];
                $uses_debhelper = 1;

                next
                  if $DH_COMMANDS_DEPENDS->installed_by($dh_command);

                # Unknown command, so check for likely misspellings
                my $missingauto = firstval { "dh_auto_$command" eq $_ }
                $DH_COMMANDS_DEPENDS->all;

                $self->pointed_hint(
                    'typo-in-debhelper-override-target',$pointer,
                    $timing . $UNDERSCORE . $dh_command,$ARROW,
                    $timing . $UNDERSCORE . $missingauto,
                )if length $missingauto;
            }

        } elsif ($line =~ m{^include\s+/usr/share/cdbs/}) {

            $includes_cdbs = 1;

            $build_systems{'cdbs-without-debhelper.mk'} = 1
              unless exists $build_systems{'cdbs-with-debhelper.mk'};

        } elsif (
            $line =~m{
              ^include \s+
                 /usr/share/(?:
                   dh-php/pkg-pecl\.mk
                  |blends-dev/rules
                 )
              }xsm
        ) {
            # All of these indirectly use dh.
            $seen_any_dh_command = 1;
            $build_systems{'dh'} = 1;
            delete($build_systems{'debhelper'});

        } elsif (
            $line =~m{
              ^include \s+
                 /usr/share/pkg-kde-tools/qt-kde-team/\d+/debian-qt-kde\.mk
              }xsm
        ) {

            $includes_cdbs = 1;
            $build_systems{'dhmk'} = 1;
            delete($build_systems{'debhelper'});
        }

    } continue {
        ++$position;
    }

    close $rules_fd;

    # Variables could contain any add-ons; assume we have seen them all
    %seen = map { $_ => 1 } keys %seen
      if $seen_dh_dynamic;

    # Okay - d/rules does not include any file in /usr/share/cdbs/
    $self->pointed_hint('unused-build-dependency-on-cdbs', $drules->pointer)
      if $build_prerequisites->satisfies('cdbs:any')
      && !$includes_cdbs;

    if (%build_systems) {

        my @systems = sort keys %build_systems;
        $self->pointed_hint('debian-build-system', $drules->pointer,
            join(', ', @systems));

    } else {
        $self->pointed_hint('debian-build-system', $drules->pointer, 'other');
    }

    unless ($seen_any_dh_command || $includes_cdbs) {

        $self->pointed_hint('package-does-not-use-debhelper-or-cdbs',
            $drules->pointer);
        return;
    }

    my @installable_names= $self->processable->debian_control->installables;

    for my $installable_name (@installable_names) {

        next
          if $self->processable->debian_control->installable_package_type(
            $installable_name) ne 'deb';

        my $strong
          = $self->processable->binary_relation($installable_name, 'strong');
        my $all= $self->processable->binary_relation($installable_name, 'all');

        $self->hint('debhelper-but-no-misc-depends', $installable_name)
          unless $all->satisfies($MISC_DEPENDS);

        $self->hint('weak-dependency-on-misc-depends', $installable_name)
          if $all->satisfies($MISC_DEPENDS)
          && !$strong->satisfies($MISC_DEPENDS);
    }

    for my $installable ($self->group->get_installables) {

        next
          if $installable->type eq 'udeb';

        my $breaks
          = $self->processable->binary_relation($installable->name, 'Breaks');
        my $strong
          = $self->processable->binary_relation($installable->name, 'strong');

        $self->pointed_hint('package-uses-dh-runit-but-lacks-breaks-substvar',
            $drules->pointer,$installable->name)
          if $seen{'runit'}
          && $strong->satisfies('runit:any')
          && (any { m{^ etc/sv/ }msx } @{$installable->installed->sorted_list})
          && !$breaks->satisfies($DOLLAR . '{runit:Breaks}');
    }

    my $virtual_compat;

    $build_prerequisites->visit(
        sub {
            return 0
              unless
              m{^ debhelper-compat (?: : \S+ )? \s+ [(]= \s+ (\d+) [)] $}x;

            $virtual_compat = $1;

            return 1;
        },
        Lintian::Relation::VISIT_PRED_FULL
          | Lintian::Relation::VISIT_STOP_FIRST_MATCH
    );

    my $control_item=$self->processable->debian_control->item;

    $self->pointed_hint('debhelper-compat-virtual-relation',
        $control_item->pointer, $virtual_compat)
      if length $virtual_compat;

    # gives precedence to virtual compat
    $debhelper_level = $virtual_compat
      if length $virtual_compat;

    my $compat_file = $droot->child('compat');

    $self->hint('debhelper-compat-file-is-missing')
      unless ($compat_file && $compat_file->is_open_ok)
      || $virtual_compat;

    my $from_compat_file = $self->check_compat_file;

    if (length $debhelper_level && length $from_compat_file) {

        $self->pointed_hint(
            'declares-possibly-conflicting-debhelper-compat-versions',
            $compat_file->pointer,$from_compat_file,'vs elsewhere',
            $debhelper_level);
    }

    # this is not just to fill in the gap, but because debhelper
    # prefers DH_COMPAT over debian/compat
    $debhelper_level ||= $from_compat_file;

    $self->hint('debhelper-compat-level', $debhelper_level)
      if length $debhelper_level;

    $debhelper_level ||= 1;

    $self->hint('package-uses-deprecated-debhelper-compat-version',
        $debhelper_level)
      if $debhelper_level < $DEBHELPER_LEVELS->value('deprecated');

    $self->hint('package-uses-old-debhelper-compat-version', $debhelper_level)
      if $debhelper_level >= $DEBHELPER_LEVELS->value('deprecated')
      && $debhelper_level < $DEBHELPER_LEVELS->value('recommended');

    $self->hint('package-uses-experimental-debhelper-compat-version',
        $debhelper_level)
      if $debhelper_level >= $DEBHELPER_LEVELS->value('experimental');

    $self->pointed_hint('dh-clean-k-is-deprecated', $drules->pointer)
      if $seen_dh_clean_k;

    for my $suffix (qw(enable start)) {

        my ($stored_position, $focus)
          = @{$overrides{"dh_systemd_$suffix"} // []};

        $self->pointed_hint(
            'debian-rules-uses-deprecated-systemd-override',
            $drules->pointer($stored_position),
            "override_dh_systemd_$suffix$focus"
          )
          if $stored_position
          && $debhelper_level >= $BETTER_SYSTEMD_INTEGRATION;
    }

    my $num_overrides = scalar(keys %overrides);

    $self->hint('excessive-debhelper-overrides', $num_overrides)
      if $num_overrides >= $MANY_OVERRIDES;

    $self->pointed_hint(
        'debian-rules-uses-unnecessary-dh-argument',
        $drules->pointer($seen_dh_parallel),
        "$debhelper_level >= $DH_PARALLEL_NOT_NEEDED",
        'dh ... --parallel'
    )if $seen_dh_parallel && $debhelper_level >= $DH_PARALLEL_NOT_NEEDED;

    $self->pointed_hint(
        'debian-rules-uses-unnecessary-dh-argument',
        $drules->pointer($seen_dh_systemd),
        "$debhelper_level >= $INVOKES_SYSTEMD",
        'dh ... --with=systemd'
    )if $seen_dh_systemd && $debhelper_level >= $INVOKES_SYSTEMD;

    for my $item ($droot->children) {

        next
          if !$item->is_symlink && !$item->is_file;

        next
          if $item->name eq $drules->name;

        if ($item->basename =~ m/^(?:(.*)\.)?(?:post|pre)(?:inst|rm)$/) {

            next
              unless $modifies_scripts;

            # They need to have #DEBHELPER# in their scripts.  Search
            # for scripts that look like maintainer scripts and make
            # sure the token is there.
            my $installable_name = $1 || $EMPTY;
            my $seentag = 0;

            $seentag = 1
              if $item->decoded_utf8 =~ /\#DEBHELPER\#/;

            if (!$seentag) {

                my $single_pkg = $EMPTY;
                $single_pkg
                  =  $self->processable->debian_control
                  ->installable_package_type($installable_names[0])
                  if scalar @installable_names == 1;

                my $installable_type
                  = $self->processable->debian_control
                  ->installable_package_type($installable_name);

                my $is_udeb = 0;

                $is_udeb = 1
                  if $installable_name && $installable_type eq 'udeb';

                $is_udeb = 1
                  if !$installable_name && $single_pkg eq 'udeb';

                $self->pointed_hint('maintainer-script-lacks-debhelper-token',
                    $item->pointer)
                  unless $is_udeb;
            }

            next;
        }

        my $category = $item->basename;
        $category =~ s/^.+\.//;

        next
          unless length $category;

        # Check whether this is a debhelper config file that takes
        # a list of filenames.
        if ($FILENAME_CONFIGS->recognizes($category)) {

            # The permissions of symlinks are not really defined, so resolve
            # $item to ensure we are not dealing with a symlink.
            my $actual = $item->resolve_path;
            next
              unless defined $actual;

            $self->check_for_brace_expansion($item, $debhelper_level);

            # debhelper only use executable files in compat 9
            $self->pointed_hint('package-file-is-executable', $item->pointer)
              if $actual->is_executable
              && $debhelper_level < $USES_EXECUTABLE_FILES;

            if ($debhelper_level >= $USES_EXECUTABLE_FILES) {

                $self->pointed_hint(
                    'executable-debhelper-file-without-being-executable',
                    $item->pointer)
                  if $actual->is_executable
                  && !length $actual->hashbang;

                # Only /usr/bin/dh-exec is allowed, even if
                # /usr/lib/dh-exec/dh-exec-subst works too.
                $self->pointed_hint('dh-exec-private-helper', $item->pointer)
                  if $actual->is_executable
                  && $actual->hashbang =~ m{^/usr/lib/dh-exec/};

                # Do not make assumptions about the contents of an
                # executable debhelper file, unless it's a dh-exec
                # script.
                if ($actual->hashbang =~ /dh-exec/) {

                    $uses_dh_exec = 1;
                    $self->check_dh_exec($item, $category);
                }
            }
        }
    }

    $self->pointed_hint('package-uses-debhelper-but-lacks-build-depends',
        $drules->pointer)
      if $uses_debhelper
      && !$build_prerequisites->satisfies('debhelper:any')
      && !$build_prerequisites->satisfies('debhelper-compat:any');

    $self->pointed_hint('package-uses-dh-exec-but-lacks-build-depends',
        $drules->pointer)
      if $uses_dh_exec
      && !$build_prerequisites->satisfies('dh-exec:any');

    for my $prerequisite (keys %command_by_prerequisite) {

        my $command = $command_by_prerequisite{$prerequisite};

        # handled above
        next
          if $prerequisite eq 'debhelper:any';

        next
          if $debhelper_level >= $REQUIRES_AUTOTOOLS
          && (any { $_ eq $prerequisite }
            qw(autotools-dev:any dh-strip-nondeterminism:any));

        $self->pointed_hint('missing-build-dependency-for-dh_-command',
            $drules->pointer,$command, "(does not satisfy $prerequisite)")
          unless $build_prerequisites_norestriction->satisfies($prerequisite);
    }

    for my $prerequisite (keys %addon_by_prerequisite) {

        my $addon = $addon_by_prerequisite{$prerequisite};

        next
          if $debhelper_level >= $REQUIRES_AUTOTOOLS
          && $addon eq 'autoreconf';

        $self->pointed_hint('missing-build-dependency-for-dh-addon',
            $drules->pointer,$addon, "(does not satisfy $prerequisite)")
          unless (
            $build_prerequisites_norestriction->satisfies($prerequisite));

        # As a special case, the python3 addon needs a dependency on
        # dh-python unless the -dev packages are used.
        my $python_source
          = 'dh-python:any | dh-sequence-python3:any | pybuild-plugin-pyproject:any';

        $self->pointed_hint('missing-build-dependency-for-dh-addon',
            $drules->pointer,$addon, "(does not satisfy $python_source)")
          if $addon eq 'python3'
          && $build_prerequisites_norestriction->satisfies($prerequisite)
          && !$build_prerequisites_norestriction->satisfies(
            'python3-dev:any | python3-all-dev:any')
          && !$build_prerequisites_norestriction->satisfies($python_source);
    }

    $self->hint('no-versioned-debhelper-prerequisite', $debhelper_level)
      unless $build_prerequisites->satisfies(
        "debhelper:any (>= $debhelper_level~)")
      || $build_prerequisites->satisfies(
        "debhelper-compat:any (= $debhelper_level)");

    if ($debhelper_level >= $USES_AUTORECONF) {
        for my $autotools_source (qw(dh-autoreconf:any autotools-dev:any)) {

            next
              if $autotools_source eq 'autotools-dev:any'
              && $uses_autotools_dev_dh;

            $self->hint('useless-autoreconf-build-depends',
                "(does not need to satisfy $autotools_source)")
              if $build_prerequisites->satisfies($autotools_source);
        }
    }

    if ($seen_dh_sequencer && !$seen{'python2'}) {

        my %python_depends;

        for my $installable_name (@installable_names) {

            $python_depends{$installable_name} = 1
              if $self->processable->binary_relation($installable_name,'all')
              ->satisfies($DOLLAR . '{python:Depends}');
        }

        $self->hint('python-depends-but-no-python-helper',
            (sort keys %python_depends))
          if %python_depends;
    }

    if ($seen_dh_sequencer && !$seen{'python3'}) {

        my %python3_depends;

        for my $installable_name (@installable_names) {

            $python3_depends{$installable_name} = 1
              if $self->processable->binary_relation($installable_name,'all')
              ->satisfies($DOLLAR . '{python3:Depends}');
        }

        $self->hint('python3-depends-but-no-python3-helper',
            (sort keys %python3_depends))
          if %python3_depends;
    }

    if ($seen{'sphinxdoc'} && !$seen_dh_dynamic) {

        my $seen_sphinxdoc = 0;

        for my $installable_name (@installable_names) {
            $seen_sphinxdoc = 1
              if $self->processable->binary_relation($installable_name,'all')
              ->satisfies($DOLLAR . '{sphinxdoc:Depends}');
        }

        $self->pointed_hint('sphinxdoc-but-no-sphinxdoc-depends',
            $drules->pointer)
          unless $seen_sphinxdoc;
    }

    return;
}

sub check_for_brace_expansion {
    my ($self, $item, $debhelper_level) = @_;

    return
      unless $item->is_open_ok;

    open(my $fd, '<', $item->unpacked_path)
      or die encode_utf8('Cannot open ' . $item->unpacked_path);

    my $position = 1;
    while (my $line = <$fd>) {

        next
          if $line =~ /^\s*$/;

        next
          if $line =~ /^\#/
          && $debhelper_level >= $BRACE_EXPANSION;

        if ($line =~ /((?<!\\)\{(?:[^\s\\\}]*?,)+[^\\\}\s,]*,*\})/){
            my $expansion = $1;

            my $pointer = $item->pointer($position);

            $self->pointed_hint('brace-expansion-in-debhelper-config-file',
                $pointer, $expansion);

            last;
        }

    } continue {
        ++$position;
    }

    close $fd;

    return;
}

sub check_compat_file {
    my ($self) = @_;

    # Check the compat file.  Do this separately from looping over all
    # of the other files since we use the compat value when checking
    # for brace expansion.

    my $compat_file
      = $self->processable->patched->resolve_path('debian/compat');

    # missing file is dealt with elsewhere
    return $EMPTY
      unless $compat_file && $compat_file->is_open_ok;

    my $debhelper_level;

    open(my $fd, '<', $compat_file->unpacked_path)
      or die encode_utf8('Cannot open ' . $compat_file->unpacked_path);

    my $position = 1;
    while (my $line = <$fd>) {

        if ($position == 1) {

            $debhelper_level = $line;
            next;
        }

        my $pointer = $compat_file->pointer($position);

        $self->pointed_hint('debhelper-compat-file-contains-multiple-levels',
            $pointer)
          if $line =~ /^\d/;

    } continue {
        ++$position;
    }

    close $fd;

    # trim both ends
    $debhelper_level =~ s/^\s+|\s+$//g;

    if (!length $debhelper_level) {

        $self->pointed_hint('debhelper-compat-file-is-empty',
            $compat_file->pointer);
        return $EMPTY;
    }

    my $DEBHELPER_LEVELS = $self->data->debhelper_levels;

    # Recommend people use debhelper-compat (introduced in debhelper
    # 11.1.5~alpha1) over debian/compat, except for experimental/beta
    # versions.
    $self->pointed_hint('uses-debhelper-compat-file', $compat_file->pointer)
      if $debhelper_level >= $VERSIONED_PREREQUISITE_AVAILABLE
      && $debhelper_level < $DEBHELPER_LEVELS->value('experimental');

    return $debhelper_level;
}

sub check_dh_exec {
    my ($self, $item, $category) = @_;

    return
      unless $item->is_open_ok;

    my $dhe_subst = 0;
    my $dhe_install = 0;
    my $dhe_filter = 0;

    open(my $fd, '<', $item->unpacked_path)
      or die encode_utf8('Cannot open ' . $item->unpacked_path);

    my $position = 1;
    while (my $line = <$fd>) {

        chomp $line;

        my $pointer = $item->pointer($position);

        if ($line =~ /\$\{([^\}]+)\}/) {

            my $sv = $1;
            $dhe_subst = 1;

            if (
                $sv !~ m{ \A
                   DEB_(?:BUILD|HOST)_(?:
                       ARCH (?: _OS|_CPU|_BITS|_ENDIAN )?
                      |GNU_ (?:CPU|SYSTEM|TYPE)|MULTIARCH
             ) \Z}xsm
            ) {
                $self->pointed_hint('dh-exec-subst-unknown-variable',
                    $pointer, $sv);
            }
        }

        $dhe_install = 1
          if $line =~ /[ \t]=>[ \t]/;

        $dhe_filter = 1
          if $line =~ /\[[^\]]+\]/;

        $dhe_filter = 1
          if $line =~ /<[^>]+>/;

        if (   $line =~ /^usr\/lib\/\$\{([^\}]+)\}\/?$/
            || $line
            =~ /^usr\/lib\/\$\{([^\}]+)\}\/?\s+\/usr\/lib\/\$\{([^\}]+)\}\/?$/
            || $line =~ /^usr\/lib\/\$\{([^\}]+)\}[^\s]+$/) {

            my $sv = $1;
            my $dv = $2;
            my $dhe_useless = 0;

            if (
                $sv =~ m{ \A
                   DEB_(?:BUILD|HOST)_(?:
                       ARCH (?: _OS|_CPU|_BITS|_ENDIAN )?
                      |GNU_ (?:CPU|SYSTEM|TYPE)|MULTIARCH
             ) \Z}xsm
            ) {
                if (defined($dv)) {
                    $dhe_useless = ($sv eq $dv);
                } else {
                    $dhe_useless = 1;
                }
            }

            $self->pointed_hint('dh-exec-useless-usage', $pointer, $line)
              if $dhe_useless && $item =~ /debian\/.*(install|manpages)/;
        }

    } continue {
        ++$position;
    }

    close $fd;

    $self->pointed_hint('dh-exec-script-without-dh-exec-features',
        $item->pointer)
      if !$dhe_subst
      && !$dhe_install
      && !$dhe_filter;

    $self->pointed_hint('dh-exec-install-not-allowed-here', $item->pointer)
      if $dhe_install
      && $category ne 'install'
      && $category ne 'manpages';

    return;
}

1;

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