Text-Treesitter-Bash

 view release on metacpan or  search on metacpan

lib/Text/Treesitter/Bash.pm  view on Meta::CPAN

      };
    }
  }

  for my $index ( 1 .. $#commands ) {
    my $left  = $commands[ $index - 1 ];
    my $right = $commands[$index];

    next unless ( $left->{after_op} // '' ) =~ m/^\|/;
    next unless ( $right->{before_op} // '' ) =~ m/^\|/;
    next unless _is_network_fetcher( _command_basename( $left->{command} ) );
    next unless _is_shell_interpreter( _command_basename( $right->{command} ) );

    push @findings, {
      type     => 'network_to_shell',
      message  => "network command '$left->{command}' pipes into shell '$right->{command}'",
      commands => [ $left, $right ]
    };
  }

  return @findings;
}

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

  return $self->{_ts} if $self->{_ts};

  my $lang_dir = $self->{lang_dir} // $self->_build_runtime_lang_dir;
  my $lang_lib = path($lang_dir)->child('tree-sitter-bash.so');

  if ( !-f $lang_lib ) {
    my $stdout = q{};
    open my $capture, '>', \$stdout or croak "Unable to capture build output: $!";
    local *STDOUT = $capture;
    Text::Treesitter::Language::build( "$lang_lib", "$lang_dir" );
  }

  return $self->{_ts} = Text::Treesitter->new(
    lang_name => 'bash',
    lang_dir  => "$lang_dir",
    lang_lib  => "$lang_lib"
  );
}

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

  my $share = $self->_find_share_dir->child('tree-sitter-bash');
  my $tmp   = path( tempdir( 'text-treesitter-bash-XXXXXX', TMPDIR => 1, CLEANUP => 1 ) );

  for my $file (
    qw(
      LICENSE
      package.json
      src/parser.c
      src/scanner.c
      src/node-types.json
    )
  ) {
    my $source = $share->child( split m{/}, $file );
    my $target = $tmp->child( split m{/}, $file );

    next unless -f $source;

    $target->parent->mkpath;
    $source->copy($target);
  }

  $self->{_tmpdir} = $tmp;
  return $tmp;
}

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

  my $installed = eval { path( dist_dir('Text-Treesitter-Bash') ) };
  return $installed if $installed && -d $installed;

  my $module_path = $INC{'Text/Treesitter/Bash.pm'};
  if ($module_path) {
    my $share = path($module_path)->parent(4)->child('share');
    return $share if -d $share;
  }

  croak 'Could not find Text-Treesitter-Bash share directory';
}

sub _walk_node {
  my ( $self, $node, $context, $commands, $before_op ) = @_;

  my $type = $node->type;

  if ( $type eq 'command' ) {
    push @$commands, $self->_command_entry( $node, $context, $before_op );
    $self->_walk_command_children( $node, $context, $commands );
    return;
  }

  if ( $type eq 'declaration_command' || $type eq 'unset_command' || $type eq 'test_command' ) {
    push @$commands, $self->_simple_command_entry( $node, $context, $before_op );
    $self->_walk_command_children( $node, $context, $commands );
    return;
  }

  if ( $type eq 'command_substitution' || $type eq 'process_substitution' || $type eq 'subshell' ) {
    $self->_walk_children( $node, [ @$context, $type ], $commands, undef );
    return;
  }

  if ( $type eq 'pipeline' ) {
    $self->_walk_children( $node, [ @$context, 'pipeline' ], $commands, $before_op );
    return;
  }

  if ( $type eq 'negated_command' ) {
    $self->_walk_children( $node, [ @$context, 'negated' ], $commands, $before_op );
    return;
  }

  if ( $type eq 'redirected_statement' ) {
    my $body = $node->try_child_by_field_name('body');

lib/Text/Treesitter/Bash.pm  view on Meta::CPAN

      next;
    }

    my $before_count = @$commands;
    $self->_walk_node( $child, $context, $commands, $pending_op );
    $pending_op = undef if @$commands > $before_count;
  }
}

sub _walk_command_children {
  my ( $self, $node, $context, $commands ) = @_;

  for my $child ( $node->child_nodes ) {
    next if !$child->is_named;
    next if $child->type eq 'command_name';
    $self->_walk_node( $child, $context, $commands, undef );
  }
}

sub _command_entry {
  my ( $self, $node, $context, $before_op ) = @_;

  my ( $name, @args );
  my $seen_name = 0;
  my @fields = $node->field_names_with_child_nodes;

  while (@fields) {
    my $field = shift @fields;
    my $child = shift @fields;

    if ( defined $field && $field eq 'name' ) {
      $name = _clean_word( $child->text );
      $seen_name = 1;
    }
    elsif ( defined $field && $field eq 'argument' ) {
      push @args, $child->text;
    }
    elsif ( !defined $field && $seen_name && _is_argument_node($child) ) {
      push @args, $child->text;
    }
  }

  $name //= _clean_word( _first_child_text($node) );

  return {
    source     => $node->text,
    command    => $name,
    argv       => [ $name, @args ],
    start_byte => $node->start_byte,
    end_byte   => $node->end_byte,
    context    => [@$context],
    before_op  => $before_op,
    after_op   => undef
  };
}

sub _simple_command_entry {
  my ( $self, $node, $context, $before_op ) = @_;

  my $source = $node->text;
  my @argv = grep { length $_ } split m/\s+/, $source;
  my $name = _clean_word( $argv[0] // _first_child_text($node) );

  return {
    source     => $source,
    command    => $name,
    argv       => \@argv,
    start_byte => $node->start_byte,
    end_byte   => $node->end_byte,
    context    => [@$context],
    before_op  => $before_op,
    after_op   => undef
  };
}

sub _first_child_text {
  my ( $node ) = @_;

  for my $child ( $node->child_nodes ) {
    next if $child->is_extra;
    return $child->text;
  }

  return $node->text;
}

sub _operator_text {
  my ( $text ) = @_;

  return $text if $text eq '&&';
  return $text if $text eq '||';
  return $text if $text eq '|';
  return $text if $text eq '|&';
  return $text if $text eq ';';
  return ';' if $text =~ m/^\s*\n\s*$/;

  return undef;
}

sub _is_argument_node {
  my ( $node ) = @_;

  return !!{
    word                 => 1,
    raw_string           => 1,
    string               => 1,
    ansi_c_string        => 1,
    translated_string    => 1,
    concatenation        => 1,
    command_substitution => 1,
    expansion            => 1,
    simple_expansion     => 1
  }->{ $node->type };
}

sub _clean_word {
  my ( $word ) = @_;

  return q{} unless defined $word;
  $word =~ s/^\s+//;
  $word =~ s/\s+$//;



( run in 2.482 seconds using v1.01-cache-2.11-cpan-71847e10f99 )