summaryrefslogtreecommitdiffstats
path: root/chrome/common/extensions/PRESUBMIT.py
blob: 491c37a93ce2ea637558a94c276c3885167fc1c9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os.path  # for initializing constants

# Directories that we run presubmit checks on.
PRESUBMIT_PATH = os.path.normpath('chrome/common/extensions/PRESUBMIT.py')
API_DIR = os.path.normpath('chrome/common/extensions/api')
DOC_DIR = os.path.normpath('chrome/common/extensions/docs')
BUILD_DIR = os.path.join(DOC_DIR, 'build')
TEMPLATE_DIR = os.path.join(DOC_DIR, 'template')
JS_DIR = os.path.join(DOC_DIR, 'js')
CSS_DIR = os.path.join(DOC_DIR, 'css')
STATIC_DIR = os.path.join(DOC_DIR, 'static')
SAMPLES_DIR = os.path.join(DOC_DIR, 'examples')

EXCEPTIONS = ['README', 'README.txt', 'OWNERS']

# Presubmit messages.
README = os.path.join(DOC_DIR, 'README.txt')
REBUILD_WARNING = (
    'This change modifies the extension docs but the generated docs have '
    'not been updated properly. See %s for more info.' % README)
BUILD_SCRIPT = os.path.join(BUILD_DIR, 'build.py')
REBUILD_INSTRUCTIONS = (
    'First build DumpRenderTree, then update the docs by running:\n  %s'
    ' --page-name=<apiName>' %
    BUILD_SCRIPT)


def CheckChangeOnUpload(input_api, output_api):
  return (CheckPresubmitChanges(input_api, output_api) +
          CheckDocChanges(input_api, output_api))

def CheckChangeOnCommit(input_api, output_api):
  return (CheckPresubmitChanges(input_api, output_api) +
          CheckDocChanges(input_api, output_api, strict=False))

def CheckPresubmitChanges(input_api, output_api):
  for PRESUBMIT_PATH in input_api.LocalPaths():
    return input_api.canned_checks.RunUnitTests(input_api, output_api,
                                                ['./PRESUBMIT_test.py'])
  return []

def CheckDocChanges(input_api, output_api, strict=True):
  warnings = []

  for af in input_api.AffectedFiles():
    path = af.LocalPath()
    if IsSkippedFile(path, input_api):
      continue

    elif (IsApiFile(path, input_api) or
          IsBuildFile(path, input_api) or
          IsTemplateFile(path, input_api) or
          IsJsFile(path, input_api) or
          IsCssFile(path, input_api)):
      # These files do not always cause changes to the generated docs
      # so we can ignore them if not running strict checks.
      if strict and not DocsGenerated(input_api):
        warnings.append('Docs out of sync with %s changes.' % path)

    elif IsStaticDoc(path, input_api):
      if not StaticDocBuilt(af, input_api):
        warnings.append('Changes to %s not reflected in generated doc.' % path)

    elif IsSampleFile(path, input_api):
      if not SampleZipped(af, input_api):
        warnings.append('Changes to sample %s have not been re-zipped.' % path)

    elif IsGeneratedDoc(path, input_api):
      if not NonGeneratedFilesEdited(input_api):
        warnings.append('Changes to generated doc %s not reflected in '
                        'non-generated files.' % path)

  if warnings:
    warnings.sort()
    warnings = [' - %s\n' % w for w in warnings]
    # Prompt user if they want to continue.
    return [output_api.PresubmitPromptWarning(REBUILD_WARNING + '\n' +
                                              ''.join(warnings) +
                                              REBUILD_INSTRUCTIONS)]
  return []

def IsSkippedFile(path, input_api):
  return input_api.os_path.basename(path) in EXCEPTIONS

def IsApiFile(path, input_api):
  return (input_api.os_path.dirname(path) == API_DIR and
          (path.endswith('.json') or path.endswith('.idl')))

def IsBuildFile(path, input_api):
  return input_api.os_path.dirname(path) == BUILD_DIR

def IsTemplateFile(path, input_api):
  return input_api.os_path.dirname(path) == TEMPLATE_DIR

def IsJsFile(path, input_api):
  return (input_api.os_path.dirname(path) == JS_DIR and
          path.endswith('.js'))

def IsCssFile(path, input_api):
  return (input_api.os_path.dirname(path) == CSS_DIR and
          path.endswith('.css'))

def IsStaticDoc(path, input_api):
  return (input_api.os_path.dirname(path) == STATIC_DIR and
          path.endswith('.html'))

def IsSampleFile(path, input_api):
  return input_api.os_path.dirname(path).startswith(SAMPLES_DIR)

def IsGeneratedDoc(path, input_api):
  return (input_api.os_path.dirname(path) == DOC_DIR and
          path.endswith('.html'))

def DocsGenerated(input_api):
  """Return True if there are any generated docs in this change.

  Generated docs are the files that are the output of build.py. Typically
  all docs changes will contain both generated docs and non-generated files.
  """
  return any(IsGeneratedDoc(path, input_api)
             for path in input_api.LocalPaths())

def NonGeneratedFilesEdited(input_api):
  """Return True if there are any non-generated files in this change.

  Non-generated files are those that are the input to build.py. Typically
  all docs changes will contain both non-generated files and generated docs.
  """
  return any(IsApiFile(path, input_api) or
             IsBuildFile(path, input_api) or
             IsTemplateFile(path, input_api) or
             IsJsFile(path, input_api) or
             IsCssFile(path, input_api) or
             IsStaticDoc(path, input_api) or
             IsSampleFile(path, input_api)
             for path in input_api.LocalPaths())

def StaticDocBuilt(static_file, input_api):
  """Return True if the generated doc that corresponds to the |static_file|
  is also in this change. Both files must also contain matching changes.
  """
  generated_file = _FindFileInAlternateDir(static_file, DOC_DIR, input_api)
  return _ChangesMatch(generated_file, static_file)

def _FindFileInAlternateDir(affected_file, alt_dir, input_api):
  """Return an AffectFile for the file in |alt_dir| that corresponds to
  |affected_file|.

  If the file does not exist in the is change, return None.
  """
  alt_path = _AlternateFilePath(affected_file.LocalPath(), alt_dir, input_api)
  for f in input_api.AffectedFiles():
    if f.LocalPath() == alt_path:
      return f

def _AlternateFilePath(path, alt_dir, input_api):
  """Return a path with the same basename as |path| but in |alt_dir| directory.

  This is useful for finding corresponding static and generated docs.

  Example:
    _AlternateFilePath('/foo/bar', '/alt/dir', ...) == '/alt/dir/bar')
  """
  base_name = input_api.os_path.basename(path)
  return input_api.os_path.join(alt_dir, base_name)

def _ChangesMatch(generated_file, static_file):
  """Return True if the two files contain the same textual changes.

  There may be extra generated lines and generated lines are still considered
  to "match" static ones even if they have extra formatting/text at their
  beginnings and ends.
  Line numbers may differ but order may not.
  """
  if not generated_file and not static_file:
    return True  # Neither file affected.

  if not generated_file or not static_file:
    return False  # One file missing.

  generated_changes = generated_file.ChangedContents()
  static_changes = static_file.ChangedContents()
  # ChangedContents() is a list of (line number, text) for all new lines.
  # Ignore the line number, but check that the text for each new line matches.

  next_generated = 0
  start_pos = 0
  for next_static in range(len(static_changes)):
    _, static_text = static_changes[next_static]

    # Search generated changes for this static text.
    found = False
    while not found and next_generated < len(generated_changes):
      _, generated_text = generated_changes[next_generated]
      # Text need not be identical but the entire static line should be
      # in the generated one (e.g. generated text might have extra formatting).
      found_at = generated_text[start_pos:].find(static_text)
      if found_at != -1:
        # Next search starts on the same line, after the substring matched.
        start_pos = found_at + len(static_text)
        found  = True
      else:
        next_generated += 1
        start_pos = 0

    if not found:
      return False

  return True

def SampleZipped(sample_file, input_api):
  """Return True if the zipfile that should contain |sample_file| is in
  this change.
  """
  sample_path = sample_file.LocalPath()
  for af in input_api.AffectedFiles():
    root, ext = input_api.os_path.splitext(af.LocalPath())
    if ext == '.zip' and sample_path.startswith(root):
      return True
  return False