HEX
Server: Apache/2.4.65 (Ubuntu)
System: Linux ielts-store-v2 6.8.0-1036-gcp #38~22.04.1-Ubuntu SMP Thu Aug 14 01:19:18 UTC 2025 x86_64
User: root (0)
PHP: 7.2.34-54+ubuntu20.04.1+deb.sury.org+1
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,
Upload Files
File: //snap/google-cloud-cli/current/lib/googlecloudsdk/core/document_renderers/linter_renderer.py
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Cloud SDK markdown document linter renderer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import collections
import io
import re

from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.document_renderers import text_renderer
import six


class LinterRenderer(text_renderer.TextRenderer):
  """Renders markdown to a list of lines where there is a linter error."""

  _HEADINGS_TO_LINT = (
      'NAME', 'EXAMPLES', 'DESCRIPTION', 'POSITIONAL ARGUMENTS',
      'REQUIRED FLAGS', 'OPTIONAL FLAGS', 'FLAGS', 'LIST COMMAND FLAGS')
  _NAME_WORD_LIMIT = 20
  _PERSONAL_PRONOUNS = ('me', 'we', 'I', 'us', 'he', 'she', 'him', 'her')
  _ARTICLES = ('the', 'a', 'an')
  # gcloud does not recognize the following flags as not requiring a value so
  # they would be marked as violations in _analyze_example_flags_equals.
  _NON_BOOL_FLAGS_ALLOWLIST = ('--quiet', '--help')
  _NON_COMMAND_SURFACE_GROUPS = ['gcloud topic']

  def __init__(self, *args, **kwargs):
    super(LinterRenderer, self).__init__(*args, **kwargs)
    # disable use of ANSI escape sequences while linting
    self._attr = console_attr.ConsoleAttr(encoding='ascii')
    self._bullet = self._attr.GetBullets()
    self._csi_char = None

    self._file_out = self._out  # the output file inherited from TextRenderer
    self._null_out = io.StringIO()
    self._buffer = io.StringIO()
    self._out = self._buffer
    self._analyze = {'NAME': self._analyze_name,
                     'EXAMPLES': self._analyze_examples,
                     'DESCRIPTION': self._analyze_description,
                     'POSITIONAL ARGUMENTS': self._analyze_argument_sections,
                     'REQUIRED FLAGS': self._analyze_argument_sections,
                     'OPTIONAL FLAGS': self._analyze_argument_sections,
                     'FLAGS': self._analyze_argument_sections,
                     'LIST COMMAND FLAGS': self._analyze_argument_sections}
    self._heading = ''
    self._prev_heading = ''
    self._example_errors = False
    self._has_example_section = False
    self.example = False
    self.command_name = ''
    self.name_section = ''
    self.command_name_length = 0
    self.command_text = ''
    self.equals_violation_flags = []
    self.nonexistent_violation_flags = []
    self.findings = collections.OrderedDict()

  def _CaptureOutput(self, heading):
    # check if buffer is full from previous heading
    self.check_indentation_for_examples()
    if self._buffer.getvalue() and self._prev_heading:
      self._Analyze(self._prev_heading, self._buffer.getvalue())
      # refresh the StringIO()
      self._buffer = io.StringIO()
    if self._prev_heading == 'EXAMPLES':
      self.check_example_section_errors()
    self._out = self._buffer
    # save heading so can get it in next section
    self._prev_heading = self._heading

  def _DiscardOutput(self, heading):
    self._out = self._null_out

  def _Analyze(self, heading, section):
    self._analyze[heading](heading, section)

  def _check_name(self, heading, check):
    return '{}_{}_CHECK'.format(heading, check)

  def _add_failure(self, check_name, message):
    self.findings['# {} FAILED'.format(check_name)] = message

  def _add_success(self, check_name):
    self.findings['# {} SUCCESS'.format(check_name)] = ''

  def _add_no_errors_summary(self, heading):
    self.findings['There are no errors for the {} section.'.format(
        heading)] = ''

  def check_example_section_errors(self):
    """Raise violation if the examples section does not contain a valid example.

    Also, wrap up the examples section by specifying there are no errors in the
    section.

    See go/cloud-sdk-help-text#formatting.
    """
    if self.needs_example() and not self.example:
      self._add_failure(
          self._check_name('EXAMPLES', 'PRESENT'),
          'You have not included an example in the Examples section.')
    elif self._has_example_section and not self._example_errors:
      self._add_no_errors_summary('EXAMPLES')
    # Do not print the example failing sentence again
    self.example = True

  def check_for_articles(self, heading, section):
    """Raise violation if the section begins with an article.

    See go/cloud-sdk-help-text#formatting.

    Arguments:
      heading: str, the name of the section.
      section: str, the contents of the section.

    Returns:
      True if there was a violation. False otherwise.
    """
    check_name = self._check_name(heading, 'ARTICLES')
    first_word = section.split()[0]
    if first_word.lower() in self._ARTICLES:
      self._add_failure(check_name, ('Please do not start the {} section with '
                                     'an article.').format(heading))
      found_article = True
    else:
      self._add_success(check_name)
      found_article = False
    return found_article

  def check_for_personal_pronouns(self, heading, section):
    """Raise violation if the section contains personal pronouns."""
    check_name = self._check_name(heading, 'PRONOUN')
    words_in_section = set(re.compile(r'[\w/\-_]+').findall(section.lower()))
    found_pronouns = words_in_section.intersection(self._PERSONAL_PRONOUNS)
    if found_pronouns:
      found_pronouns_list = sorted(list(found_pronouns))
      self._add_failure(check_name, ('Please remove the following personal '
                                     'pronouns in the {} section:\n{}').format(
                                         heading,
                                         '\n'.join(found_pronouns_list)))
    else:
      self._add_success(check_name)
    return found_pronouns

  def check_for_unmatched_double_backticks(self, heading, section):
    """Raise violation if the section contains unmatched double backticks.

    This check counts the number of double backticks in the section and ensures
    that there are an equal number of closing double single-quotes. The common
    mistake is to use a single double-quote to close these values, which breaks
    the rendering. See go/cloud-sdk-help-text#formatting.

    Arguments:
      heading: str, the name of the section.
      section: str, the contents of the section.

    Returns:
      True if there was a violation. None otherwise.
    """
    check_name = self._check_name(heading, 'DOUBLE_BACKTICKS')
    double_backticks_count = len(re.compile(r'``').findall(section))
    double_single_quotes_count = len(re.compile(r"''").findall(section))
    unbalanced = (double_backticks_count != double_single_quotes_count)
    if unbalanced:
      self._add_failure(check_name,
                        ('There are unbalanced double backticks and double '
                         'single-quotes in the {} section. See '
                         'go/cloud-sdk-help-text#formatting.'.format(heading)))
    else:
      self._add_success(check_name)
    return unbalanced

  def needs_example(self):
    """Check whether command requires an example."""
    # alpha commands, groups, and certain directories do not need examples.
    if self.command_metadata and self.command_metadata.is_group:
      return False
    if 'alpha' in self.command_name:
      return False
    for name in self._NON_COMMAND_SURFACE_GROUPS:
      if self.command_name.startswith(name):
        return False
    return True

  def check_indentation_for_examples(self):
    if self._prev_heading == 'EXAMPLES' and not self._buffer.getvalue():
      self._add_failure(
          self._check_name('EXAMPLES', 'SECTION_FORMAT'),
          'The examples section is not formatted properly. This is likely due '
          'to indentation. Please make sure the section is aligned with the '
          'heading and not indented.')
      self._example_errors = True

  def Finish(self):
    if self._buffer.getvalue() and self._prev_heading:
      self._Analyze(self._prev_heading, self._buffer.getvalue())
    self.check_indentation_for_examples()
    self._buffer.close()
    self._null_out.close()
    self.check_example_section_errors()
    for element in self.findings:
      if self.findings[element]:
        self._file_out.write(
            six.text_type(element) + ': ' +
            six.text_type(self.findings[element]) + '\n')
      else:
        self._file_out.write(six.text_type(element) + '\n')

  def Heading(self, level, heading):
    self._heading = heading
    if heading in self._HEADINGS_TO_LINT:
      self._CaptureOutput(heading)
    else:
      self._DiscardOutput(heading)

  def Example(self, line):
    # ensure this example is in the EXAMPLES section and it is not a group level
    # command
    if (self.command_metadata and not self.command_metadata.is_group and
        self._heading == 'EXAMPLES'):
      # if previous line ended in a backslash, it is not the last line of the
      # command so append new line of command to command_text
      if self.command_text and self.command_text.endswith('\\'):
        self.command_text = self.command_text.rstrip('\\') + line.strip()
      # This is the first line of the command and ignore the `$ ` in it.
      else:
        self.command_text = line.replace('$ ', '')
      # if the current line doesn't end with a `\`, it is the end of the command
      # so self.command_text is the whole command
      if not line.endswith('\\'):
        # check that the example starts with the command of the help text
        if self.command_text.startswith(self.command_name):
          self.example = True
          self._add_success(self._check_name('EXAMPLES', 'PRESENT'))
          rest_of_command = self.command_text[self.command_name_length:].split()
          flag_names = []
          for word in rest_of_command:
            word = word.replace('\\--', '--')
            # Stop parsing arguments when ' -- ' is encountered.
            if word == '--':
              break
            if word.startswith('--'):
              flag_names.append(word)
          self._analyze_example_flags_equals(flag_names)
          flags = [flag.partition('=')[0] for flag in flag_names]
          if self.command_metadata and self.command_metadata.flags:
            self._check_valid_flags(flags)

  def _check_valid_flags(self, flags):
    for flag in flags:
      if flag not in self.command_metadata.flags:
        self.nonexistent_violation_flags.append(flag)

  def _analyze_example_flags_equals(self, flags):
    for flag in flags:
      if ('=' not in flag and flag not in self.command_metadata.bool_flags and
          flag not in self._NON_BOOL_FLAGS_ALLOWLIST):
        self.equals_violation_flags.append(flag)

  def _analyze_argument_sections(self, heading, section):
    """Raise violation if the section contains unmatched double backticks.

    This check confirms that argument sections follow our help text style guide.
    The help text for individual arguments should not begin with an article.
    See go/cloud-sdk-help-text#formatting.

    Arguments:
      heading: str, the name of the section.
      section: str, the contents of the section.

    Returns:
      None.
    """
    has_errors = (self.check_for_personal_pronouns(heading, section) or
                  self.check_for_articles(heading, section))
    check_name = self._check_name(heading, 'ARG_ARTICLES')
    flags_with_articles = []
    all_lines_in_section = section.split('\n')
    non_empty_lines_in_section = [
        line.strip() for line in all_lines_in_section if (
            not line.isspace() and line)]
    prev_line = ''
    for line in non_empty_lines_in_section:
      if prev_line and (prev_line.startswith('--') or re.match(
          '[A-Z_]', prev_line.split()[0])) and len(prev_line.split()) < 5 and (
              line.split()[0].lower() in self._ARTICLES):
        flags_with_articles.append(prev_line)
      prev_line = line

    if flags_with_articles:
      has_errors = True
      self._add_failure(check_name, ('Please fix the help text for the '
                                     'following arguments which begin with an '
                                     'article in the {} section:\n{}').format(
                                         heading,
                                         '\n'.join(flags_with_articles)))
    else:
      self._add_success(check_name)
    if not has_errors:
      self._add_no_errors_summary(heading)

  def _analyze_name(self, heading, section):
    has_errors = (self.check_for_personal_pronouns(heading, section) or
                  self.check_for_articles(heading, section))

    # The section should look like 'command name - command description' but
    # there may be a newline depending on length of the command.
    section_parts = re.split(r'\s-\s?', section.strip())

    # This is checking if there is a short description in the NAME section. The
    # section_parts list may have whitespace as the second element but this is
    # not a description.
    check_name = self._check_name('NAME', 'DESCRIPTION')
    if len(section_parts) == 1 or (
        len(section_parts) > 1 and not section_parts[1].strip()):
      self.name_section = ''
      self._add_failure(check_name,
                        'Please add an explanation for the command.')
      has_errors = True
    else:
      self.name_section = section_parts[1]
      self._add_success(check_name)

    # check that name section is not too long
    check_name = self._check_name('NAME', 'LENGTH')
    self.command_name = ' '.join(section_parts[0].strip().split())
    self.command_name_length = len(self.command_name)
    if len(self.name_section.split()) > self._NAME_WORD_LIMIT:
      self._add_failure(
          check_name,
          'Please shorten the name section description to less than {} words.'
          .format(six.text_type(self._NAME_WORD_LIMIT)))
      has_errors = True
    else:
      self._add_success(check_name)

    if not has_errors:
      self._add_no_errors_summary(heading)

  def _analyze_examples(self, heading, section):
    self._has_example_section = True
    has_errors = self.check_for_articles(heading, section)
    if not self.command_metadata.is_group:
      if self.check_for_personal_pronouns(heading, section):
        has_errors = True
      if self.check_for_unmatched_double_backticks(heading, section):
        has_errors = True
      check_name = self._check_name(heading, 'FLAG_EQUALS')
      if self.equals_violation_flags:
        has_errors = True
        list_contents = ''
        for flag in range(len(self.equals_violation_flags) - 1):
          list_contents += six.text_type(
              self.equals_violation_flags[flag]) + ', '
        list_contents += six.text_type(self.equals_violation_flags[-1])
        self._add_failure(
            check_name,
            ('There should be an `=` between the flag name and '
             'the value for the following flags: {}').format(list_contents))
        has_errors = True
      else:
        self._add_success(check_name)
      check_name = self._check_name(heading, 'NONEXISTENT_FLAG')
      if self.nonexistent_violation_flags:
        has_errors = True
        list_contents = ''
        for flag in range(len(self.nonexistent_violation_flags) - 1):
          list_contents += six.text_type(
              self.nonexistent_violation_flags[flag]) + ', '
        list_contents += six.text_type(self.nonexistent_violation_flags[-1])
        self._add_failure(
            check_name,
            'The following flags are not valid: {}'.format(
                list_contents))
      else:
        self._add_success(check_name)
      self._example_errors = has_errors

  def _analyze_description(self, heading, section):
    has_errors = (self.check_for_personal_pronouns(heading, section),
                  self.check_for_unmatched_double_backticks(
                      heading, section),
                  self.check_for_articles(heading, section))

    if not any(has_errors):
      self._add_no_errors_summary(heading)