summaryrefslogtreecommitdiffstats
path: root/lib/Lintian/Output/JSON.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Lintian/Output/JSON.pm')
-rw-r--r--lib/Lintian/Output/JSON.pm322
1 files changed, 322 insertions, 0 deletions
diff --git a/lib/Lintian/Output/JSON.pm b/lib/Lintian/Output/JSON.pm
new file mode 100644
index 0000000..08996e2
--- /dev/null
+++ b/lib/Lintian/Output/JSON.pm
@@ -0,0 +1,322 @@
+# Copyright (C) 2020-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::Output::JSON;
+
+use v5.20;
+use warnings;
+use utf8;
+
+use Const::Fast;
+use Time::Piece;
+use JSON::MaybeXS;
+
+use Lintian::Output::Markdown qw(markdown_citation);
+
+const my $EMPTY => q{};
+
+const my %CODE_PRIORITY => (
+ 'E' => 30,
+ 'W' => 40,
+ 'I' => 50,
+ 'P' => 60,
+ 'X' => 70,
+ 'C' => 80,
+ 'O' => 90,
+ 'M' => 100,
+);
+
+use Moo;
+use namespace::clean;
+
+=head1 NAME
+
+Lintian::Output::JSON - JSON hint output
+
+=head1 SYNOPSIS
+
+ use Lintian::Output::JSON;
+
+=head1 DESCRIPTION
+
+Provides JSON hint output.
+
+=head1 INSTANCE METHODS
+
+=over 4
+
+=item issue_hints
+
+Print all hints passed in array. A separate arguments with processables
+is necessary to report in case no hints were found.
+
+=cut
+
+sub issue_hints {
+ my ($self, $profile, $groups) = @_;
+
+ $groups //= [];
+
+ my %output;
+
+ $output{lintian_version} = $ENV{LINTIAN_VERSION};
+
+ my @allgroups_output;
+ $output{groups} = \@allgroups_output;
+
+ for my $group (sort { $a->name cmp $b->name } @{$groups}) {
+
+ my %group_output;
+ $group_output{group_id} = $group->name;
+ $group_output{source_name} = $group->source_name;
+ $group_output{source_version} = $group->source_version;
+
+ push(@allgroups_output, \%group_output);
+
+ my @allfiles_output;
+ $group_output{input_files} = \@allfiles_output;
+
+ for my $processable (sort {$a->path cmp $b->path}
+ $group->get_processables) {
+
+ my %file_output;
+ $file_output{path} = $processable->path;
+ $file_output{hints}
+ = $self->hintlist($profile, $processable->hints);
+
+ push(@allfiles_output, \%file_output);
+ }
+ }
+
+ # convert to UTF-8 prior to encoding in JSON
+ my $encoder = JSON->new;
+ $encoder->canonical;
+ $encoder->utf8;
+ $encoder->pretty;
+
+ my $json = $encoder->encode(\%output);
+
+ # output encoded JSON; is already in UTF-8
+ print $json;
+
+ return;
+}
+
+=item C<hintlist>
+
+=cut
+
+sub hintlist {
+ my ($self, $profile, $arrayref) = @_;
+
+ my %sorter;
+ for my $hint (@{$arrayref // []}) {
+
+ my $tag = $profile->get_tag($hint->tag_name);
+
+ my $override_status = 0;
+ $override_status = 1
+ if defined $hint->override || @{$hint->masks};
+
+ my $ranking_code = $tag->code;
+ $ranking_code = 'X'
+ if $tag->experimental;
+ $ranking_code = 'O'
+ if defined $hint->override;
+ $ranking_code = 'M'
+ if @{$hint->masks};
+
+ my $code_priority = $CODE_PRIORITY{$ranking_code};
+
+ push(
+ @{
+ $sorter{$override_status}{$code_priority}{$tag->name}
+ {$hint->context}
+ },
+ $hint
+ );
+ }
+
+ my @sorted;
+ for my $override_status (sort keys %sorter) {
+ my %by_code_priority = %{$sorter{$override_status}};
+
+ for my $code_priority (sort { $a <=> $b } keys %by_code_priority) {
+ my %by_tag_name = %{$by_code_priority{$code_priority}};
+
+ for my $tag_name (sort keys %by_tag_name) {
+ my %by_context = %{$by_tag_name{$tag_name}};
+
+ for my $context (sort keys %by_context) {
+
+ my $hints
+ = $sorter{$override_status}{$code_priority}{$tag_name}
+ {$context};
+
+ push(@sorted, $_)for @{$hints};
+ }
+ }
+ }
+ }
+
+ my @hint_dictionaries;
+ for my $hint (@sorted) {
+
+ my $tag = $profile->get_tag($hint->tag_name);
+
+ my %hint_dictionary;
+ push(@hint_dictionaries, \%hint_dictionary);
+
+ $hint_dictionary{tag} = $tag->name;
+ $hint_dictionary{note} = $hint->note;
+
+ if ($hint->can('pointer')) {
+ my $pointer = $hint->pointer;
+
+ my %pointer_dictionary;
+
+ if ($pointer->can('item')) {
+ my $item = $pointer->item;
+
+ my %item_dictionary;
+ $item_dictionary{name} = $item->name;
+ $item_dictionary{index} = $item->index->identifier;
+
+ $pointer_dictionary{item} = \%item_dictionary;
+
+ # numerify to force JSON integer
+ # https://metacpan.org/pod/JSON::XS#simple-scalars
+ $pointer_dictionary{line_position} = $pointer->position + 0;
+ }
+
+ $hint_dictionary{pointer} = \%pointer_dictionary;
+ }
+
+ $hint_dictionary{visibility} = $tag->visibility;
+ $hint_dictionary{experimental}
+ = ($tag->experimental ? JSON()->true : JSON()->false);
+
+ for my $mask (@{ $hint->masks }) {
+
+ my %mask_dictionary;
+ $mask_dictionary{screen} = $mask->screen;
+ $mask_dictionary{excuse} = $mask->excuse;
+
+ push(@{$hint_dictionary{masks}}, \%mask_dictionary);
+ }
+
+ if ($hint->override) {
+
+ my %override_dictionary;
+ $override_dictionary{justification}
+ = $hint->override->justification;
+
+ $hint_dictionary{override} = \%override_dictionary;
+ }
+ }
+
+ return \@hint_dictionaries;
+}
+
+=item describe_tags
+
+=cut
+
+sub describe_tags {
+ my ($self, $data, $tags) = @_;
+
+ my @tag_dictionaries;
+
+ for my $tag (@{$tags}) {
+
+ my %tag_dictionary;
+ push(@tag_dictionaries, \%tag_dictionary);
+
+ $tag_dictionary{name} = $tag->name;
+ $tag_dictionary{name_spaced}
+ = ($tag->name_spaced ? JSON()->true : JSON()->false);
+ $tag_dictionary{show_always}
+ = ($tag->show_always ? JSON()->true : JSON()->false);
+
+ $tag_dictionary{explanation} = $tag->explanation;
+
+ my @tag_see_also_markdown
+ = map { markdown_citation($data, $_) } @{$tag->see_also};
+ $tag_dictionary{see_also} = \@tag_see_also_markdown;
+
+ $tag_dictionary{check} = $tag->check;
+ $tag_dictionary{visibility} = $tag->visibility;
+ $tag_dictionary{experimental}
+ = ($tag->experimental ? JSON()->true : JSON()->false);
+
+ $tag_dictionary{renamed_from} = $tag->renamed_from;
+
+ my @screen_dictionaries;
+
+ for my $screen (@{$tag->screens}) {
+
+ my %screen_dictionary;
+ push(@screen_dictionaries, \%screen_dictionary);
+
+ $screen_dictionary{name} = $screen->name;
+
+ my @advocate_emails = map { $_->format } @{$screen->advocates};
+ $screen_dictionary{advocates} = \@advocate_emails;
+
+ $screen_dictionary{reason} = $screen->reason;
+
+ my @screen_see_also_markdown
+ = map { markdown_citation($data, $_) } @{$screen->see_also};
+ $screen_dictionary{see_also} = \@screen_see_also_markdown;
+ }
+
+ $tag_dictionary{screens} = \@screen_dictionaries;
+
+ $tag_dictionary{lintian_version} = $ENV{LINTIAN_VERSION};
+ }
+
+ # convert to UTF-8 prior to encoding in JSON
+ my $encoder = JSON->new;
+ $encoder->canonical;
+ $encoder->utf8;
+ $encoder->pretty;
+
+ # encode single tags without array bracketing
+ my $object = \@tag_dictionaries;
+ $object = shift @tag_dictionaries
+ if @tag_dictionaries == 1;
+
+ my $json = $encoder->encode($object);
+
+ # output encoded JSON; is already in UTF-8
+ print $json;
+
+ return;
+}
+
+=back
+
+=cut
+
+1;
+
+# Local Variables:
+# indent-tabs-mode: nil
+# cperl-indent-level: 4
+# End:
+# vim: syntax=perl sw=4 sts=4 sr et