<filename>ui/file_manager/base/tools/modules.py
#!/usr/bin/env python
#
# Copyright (c) 2020 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
import re
import sys
import subprocess


def get_file_lines(file_path):
    '''Returns list of lines from a file. '\n' at the end of lines are
    removed.'''
    with open(file_path, 'r') as f:
        file_lines = f.readlines()
    return [l.rstrip() for l in file_lines]


def save_file_lines(file_path, file_lines):
    '''Saves file_lines in file pointed by `file_path`, '\n' at the end of
    lines are added back.'''
    with open(file_path, 'w') as f:
        for line in file_lines:
            f.write(line + '\n')


def get_relative_dependency(path, dir_path):
    '''Given a file path, returns formatted BUILD.gn dependency.

    Parameters:
        path: file path to format as relative dependency.
        dir_path: directory from which the relative dependency is calculated.

    Returns:
        Formatted dependency.
        The 3 cases illustrated below are handled:
        - ":file_type.m" if returned if file_type.js is in dir_path.
        - "metadata:metadata_model.m" is returned if metadata/metadata_model.js
        is in dir_path.
        - "//ui/file_manager/file_manager/externs:volume_manager.m" is returned if
        ui/file_manager/file_manager/externs is not included in dir_path.
    '''
    split_path = path.split('/')
    file_name = split_path.pop().replace('.js', '.m')
    split_dir_path = dir_path.split('/')
    while (len(split_path) > 0 and len(split_dir_path) > 0
           and split_path[0] == split_dir_path[0]):
        del split_path[0]
        del split_dir_path[0]
    if len(split_dir_path) == 0:
        # The dependency is within dir_path.
        return '/'.join(split_path) + ':' + file_name
    else:
        return '//' + re.sub(r"\/[a-zA-Z0-9_]+\.js", ":", path) + file_name


def get_index_substr(file_lines, substr):
    '''Finds first occurence of `substr` and returns its index in
    `file_lines`.'''
    for i, line in enumerate(file_lines):
        if substr in line:
            return i
    return -1


def get_end_of_copyright(file_lines):
    '''Get index of last line of copyright (after checking that the section
    exists).'''
    index = get_index_substr(file_lines, 'Copyright 20')
    if index < 0:
        return -1
    while index < len(file_lines) and file_lines[index].startswith('// '):
        index += 1
    return index - 1


def get_end_of_file_overview(file_lines):
    '''Get index of last line of file_overview (after checking that the section
    exists).'''
    index = get_index_substr(file_lines, '@fileoverview')
    if index < 0:
        return -1
    while index < len(file_lines) and file_lines[index] != ' */':
        index += 1
    return index


def get_last_index_of_line_starting_with(file_lines, substr):
    '''Get last line starting with the given `substr`.'''
    last_index = -1
    for i, line in enumerate(file_lines):
        if line.startswith(substr):
            last_index = i
    return last_index


def get_end_of_build_imports(file_lines):
    '''In BUILD.gn, gets index of last 'import' line at the beginning of the
    file.'''
    index = get_last_index_of_line_starting_with(file_lines, 'import("//')
    if index < 0:
        return get_end_of_copyright(file_lines)
    return index


def add_js_library(file_lines, file_name, dir_path):
    '''Adds js_library target in BUILD.gn.'''
    i = file_lines.index('js_library("%s") {' % (file_name))

    # Check that the library does exist and hasn't already been converted.
    if i < 0:
        print 'Unable to find js_library for {}'.format(file_name)
        return False
    if 'js_library("%s.m") {' % (file_name) in file_lines:
        print 'js_library for {}.m already added'.format(file_name)
        return False

    # Find the end of the library definition.
    while i < len(file_lines) and file_lines[i] != '}':
        i += 1
    i += 1
    if i == len(file_lines):
        print 'reached end of file'
        return False
    new_lines = '''
js_library("%s.m") {
  sources = [ "$root_gen_dir/%s/%s.m.js" ]

  extra_deps = [ ":modulize" ]
}''' % (file_name, dir_path, file_name)
    file_lines[i:i] = new_lines.split('\n')
    return True


def add_import_line(file_lines, variable, relative_path, is_unittest):
    '''Adds import line (import {...} from '...') in JS file.'''
    # Construct import line.
    import_line = 'import ' if is_unittest else '// #import '

    # Check: existing relative path.
    i = get_index_substr(file_lines, relative_path)
    if i >= 0 and '}' in file_lines[i]:
        if variable + '}' in file_lines[i] or variable + ',' in file_lines[i]:
            return
        split_line = file_lines[i].split('}')
        file_lines[i] = '%s, %s}%s' % (split_line[0], variable, split_line[1])
        return
    import_line += "{%s} from '%s';" % (variable, relative_path)

    if import_line in file_lines:
        return

    # Add clang-format off/on if necessary.
    index = 0
    if '// clang-format off' in file_lines:
        index = file_lines.index('// clang-format off')
    else:
        # Go to the end of copyright and fileoverview.
        index = get_end_of_file_overview(file_lines)
        if index < 0:
            index = get_end_of_copyright(file_lines)
            if (index < 0):
                index = 0
        index += 1
        if len(import_line) > 80:
            index += 1
            file_lines.insert(index, '// clang-format off')

            # Go to the last existing import line.
            last_import_line_index = get_last_index_of_line_starting_with(
                file_lines, 'import ')
            if last_import_line_index >= 0:
                index = last_import_line_index
            else:
                file_lines.insert(index + 1, '')
            file_lines.insert(index + 1, '// clang-format on')
        elif 'import ' not in file_lines[index + 1]:
            file_lines.insert(index, '')

    # Add import line.
    file_lines.insert(index + 1, import_line)


def add_namespace_rewrite(build_gn_path):
    build_file_lines = get_file_lines(build_gn_path)

    # Add import("//ui/webui/resources/js/cr.gni").
    cr_gni = 'import("//ui/webui/resources/js/cr.gni")'
    modulizer_gni = 'import("//ui/webui/resources/tools/js_modulizer.gni")'
    if not cr_gni in build_file_lines:
        if not modulizer_gni in build_file_lines:
            raise ValueError('"js_modulizer.gni" not found')
        index = build_file_lines.index(modulizer_gni)
        build_file_lines.insert(index, cr_gni)

    # Add namespace_rewrites = cr_namespace_rewrites.
    namespace_rewrite = '  namespace_rewrites = cr_namespace_rewrites'
    if not namespace_rewrite in build_file_lines:
        index = get_index_substr(build_file_lines,
                                 'js_modulizer("modulize") {')
        if index < 0:
            print 'No modulize rule found'
            return
        while index < len(build_file_lines) and build_file_lines[index] != '':
            index += 1
        index -= 1
        build_file_lines.insert(index, '')
        build_file_lines.insert(index + 1, namespace_rewrite)

    save_file_lines(build_gn_path, build_file_lines)


def add_hide_third_party(build_gn_path):
    build_file_lines = get_file_lines(build_gn_path)

    hide_third_party = '    "hide_warnings_for=third_party/",'
    prefix_replacement_line = 'browser_resolver_prefix_replacements='
    if not hide_third_party in build_file_lines:
        index = get_index_substr(build_file_lines, prefix_replacement_line)
        if index < 0:
            print 'prefix replacement not found in "js_test_gen_html_modules"'
            return
        build_file_lines.insert(index + 1, hide_third_party)

    save_file_lines(build_gn_path, build_file_lines)


def add_dependency(file_lines, rule_first_line, list_name, dependency_line):
    '''
    Add dependency in BUILD.gn.
    Parameters:
        file_lines: lines of BUILD.gn file.
        rule_first_line: opening line of target to update.
        list_name: name of the dependency list, deps', 'input_files' etc...
        dependency_line: line to add to the dependency list to update.
    '''
    # Find a line that starts with deps.
    if not rule_first_line in file_lines:
        print 'Unable to find ' + rule_first_line
        return False

    # Find index of `list_name`. Get index of 'sources = [' in case `list_name`
    # is not defined.
    rule_index = file_lines.index(rule_first_line)
    sources_index = -1
    insertion_index = -1
    single_line_dependency_list = False

    for i in range(rule_index, len(file_lines)):
        if 'sources = [' in file_lines[i]:
            # Jump to the end of the 'sources' list.
            while not ']' in file_lines[i]:
                i += 1
            sources_index = i
        if '  {} = '.format(list_name) in file_lines[i]:
            # Dependency line found.
            if file_lines[i].endswith(']'):
                single_line_dependency_list = True

            # Go to the end of the dependency list.
            while not ']' in file_lines[i]:
                if dependency_line == file_lines[i]:
                    # Dependency already found.
                    return False
                i += 1
            insertion_index = i
            break
        if file_lines[i] == '}':
            # End of build rule.
            break

    if insertion_index == -1:
        # Add dependency line, after sources if possible.
        index = sources_index + 1 if sources_index > 0 else rule_index + 1

        # Define new list over 2 lines: 'list_name = [\n]'
        file_lines.insert(index, '  {} = ['.format(list_name))
        file_lines.insert(index + 1, '  ]')
        insertion_index = index + 1

    if single_line_dependency_list:
        # Use regex to find characters between [].
        result = re.search('\[(.*)\]', file_lines[insertion_index])
        existing_dependency = result.group(1).strip()
        new_lines = '''\
  {} = [
    {},
  ]'''.format(list_name, existing_dependency)

        # Rewrite single-line dependency list.
        file_lines[insertion_index:insertion_index + 1] = new_lines.split('\n')
        insertion_index += 1

        # Check for already imported dependency after reformatting.
        if file_lines[insertion_index] == dependency_line:
            return False

        # If there was no existing dependency, remove appropriate line.
        if existing_dependency == '':
            del file_lines[insertion_index]

    # Insert dependency.
    file_lines.insert(insertion_index, dependency_line)
    return True


def update_build_gn_dependencies(dir_path, file_name, build_gn_path):
    print 'Updating BUILD.gn dependencies for ' + file_name

    # Get file contents.
    file_lines = get_file_lines(build_gn_path)

    # Edit file with modules-related targets.
    import_gni = 'import("//ui/webui/resources/tools/js_modulizer.gni")'
    if not import_gni in file_lines:
        index = get_end_of_build_imports(file_lines) + 1
        file_lines.insert(index, import_gni)
        new_lines = '''
js_modulizer("modulize") {
  input_files = [
  ]
}'''
        file_lines.extend(new_lines.split('\n'))
        if not add_dependency(file_lines, 'group("closure_compile") {', 'deps',
                              '    ":closure_compile_jsmodules",'):
            return

        # Add closure_compile_jsmodules rule.
        index = get_index_substr(file_lines,
                                 'js_type_check("closure_compile_module") {')
        if index < 0:
            print 'js_type_check("closure_compile_module") not found'
            return
        new_lines = '''\
js_type_check("closure_compile_jsmodules") {
  deps = [
  ]
}
'''
        file_lines[index:index] = new_lines.split('\n')
    if not add_js_library(file_lines, file_name, dir_path):
        return

    # Add closure dependency.
    if not add_dependency(
            file_lines, 'js_type_check("closure_compile_jsmodules") {', 'deps',
            '    ":{}.m",'.format(file_name)):
        return

    # Add 'modulize' dependency.
    if not add_dependency(file_lines, 'js_modulizer("modulize") {',
                          'input_files', '    "{}.js",'.format(file_name)):
        return

    # Save file contents.
    save_file_lines(build_gn_path, file_lines)


def update_buid_gn_unittest_dependencies(build_gn_path, js_file_path,
                                         file_name):
    '''
    Rename _unittest.js to _unittest.m.js.
    Update test URL in file_manager_jstest.cc.
    Update BUILD.gn rules.
    It is assumed that if we're converting [file]_unittest.js, [file].js has
    been converted already.

    Parameters:
        build_gn_path: Path of BUILD.gn file used by the file be converted.
        js_file_path: Path of the file to be converted
                        (ui/file_manager/.../..._unittest.m.js)
        file_name: Name of the file to be converted (..._unittest)
    '''
    print 'Updating BUILD.gn dependencies for ' + file_name

    main_build_path = 'ui/file_manager/BUILD.gn'
    file_manager_jstest_path = ('chrome/browser/chromeos/file_manager/'
                                'file_manager_jstest.cc')

    # Get file contents.
    build_file_lines = get_file_lines(build_gn_path)
    main_build_file_lines = get_file_lines(main_build_path)
    file_manager_jstest_lines = get_file_lines(file_manager_jstest_path)

    # Rename _unittest.js to _unittest.m.js.
    original_js_file_path = js_file_path.replace('.m.js', '.js')
    if not os.path.isfile(js_file_path):
        if not os.path.isfile(original_js_file_path):
            e = 'Could not find neither file {} or {}'.format(
                js_file_path, original_js_file_path)
            raise ValueError(e)
        os.rename(original_js_file_path, js_file_path)

    # Update chrome/browser/chromeos/file_manager/file_manager_jstest.cc
    # by updating RunTestURL for the unittest being converted.
    # [...]_unittest_gen.html -> [...]_unittest.m_gen.html.
    index = get_index_substr(file_manager_jstest_lines, file_name + '.m')
    if index < 0:
        index = get_index_substr(file_manager_jstest_lines, file_name)
        if index < 0:
            print 'file_manager_jstest.cc: {} not found'.format(file_name)
        file_manager_jstest_lines[index] = file_manager_jstest_lines[
            index].replace(file_name, file_name + '.m')

    # Add "js_test_gen_html_modules" target.
    build_rule = 'js_test_gen_html("js_test_gen_html_modules") {'
    if not build_rule in build_file_lines:
        if 'js_test_gen_html("js_test_gen_html") {' in build_file_lines:
            index = build_file_lines.index(
                'js_test_gen_html("js_test_gen_html") {') - 1
            new_lines = r'''
js_test_gen_html("js_test_gen_html_modules") {
  deps = [
  ]
  js_module = true

  closure_flags =
      strict_error_checking_closure_args + [
        "js_module_root=./gen/ui",
        "js_module_root=../../ui",
        "browser_resolver_prefix_replacements=\"chrome://test/=./\"",
      ]
}'''
            build_file_lines[index:index] = new_lines.split('\n')
        else:
            e = '"js_test_gen_html" build target not found on {}'.format(
                build_gn_path)
            raise ValueError(e)

    # Add ":js_test_gen_html_modules_type_check_auto", in closure_compile deps.
    index = get_index_substr(build_file_lines,
                             ':js_test_gen_html_modules_type_check_auto')
    if index < 0:
        index = get_index_substr(build_file_lines,
                                 ':js_test_gen_html_type_check_auto')
        if index < 0:
            e = '"js_test_gen_html_type_check_auto" dependency not found'
            raise ValueError(e)
        else:
            new_line = '    ":js_test_gen_html_modules_type_check_auto",'
            build_file_lines.insert(index, new_line)

    # In BUILD.gn, update js_unittest target to .m
    js_unittest = 'js_unittest("%s") {' % (file_name)
    js_unittest_m = 'js_unittest("%s.m") {' % (file_name)
    if js_unittest in build_file_lines:
        index = build_file_lines.index(js_unittest)
        build_file_lines[index] = js_unittest_m
        # Remove dependencies.
        while index < len(
                build_file_lines) and not 'deps =' in build_file_lines[index]:
            index += 1
        if ']' in build_file_lines[index]:
            build_file_lines[index] = '  deps = ['
            build_file_lines.insert(index, '  ]')
        else:
            index += 1
            while index < len(
                    build_file_lines) and not ']' in build_file_lines[index]:
                del build_file_lines[index]
    else:
        if not js_unittest_m in build_file_lines:
            print 'Unable to find "{}" target'.format(file_name)

    # Move unittest dependency from js_test_gen_html to
    # js_test_gen_html_modules.
    index = get_index_substr(build_file_lines, '":{}",'.format(file_name))
    if index >= 0:
        del build_file_lines[index]
    else:
        single_line_deps = '  deps = [ ":{}" ]'.format(file_name)
        if single_line_deps in build_file_lines:
            index = build_file_lines.index(single_line_deps)
            build_file_lines[index] = '  deps = []'

    add_dependency(build_file_lines,
                   'js_test_gen_html("js_test_gen_html_modules") {', 'deps',
                   '    ":{}.m",'.format(file_name))
    # Add dependency in ui/file_manager/BUILD.gn.
    dir_path = os.path.dirname(js_file_path)
    rel_dir_path = '/'.join(dir_path.split('/')[2:])
    dependency_line = '    "{}:js_test_gen_html_modules",'.format(rel_dir_path)
    if not dependency_line in main_build_file_lines:
        if not add_dependency(main_build_file_lines,
                              'group("unit_test_data") {', 'deps',
                              dependency_line):
            print 'unable to update ui/file_manager/BUILD.gn'

    # Save file contents.
    save_file_lines(build_gn_path, build_file_lines)
    save_file_lines(main_build_path, main_build_file_lines)
    save_file_lines(file_manager_jstest_path, file_manager_jstest_lines)


def parse_compiler_output(compiler_output, build_gn_path, file_name):
    '''Parse closure compile output and return list of token that are
    undefined/undeclared (no duplicate).'''
    print "Parsing closure compiler output"
    with open(compiler_output, 'r') as f:
        # Retrieve from the closure compile output the types that need to be
        # imported.
        variables_to_import = []
        for line in f:
            line = line.rstrip()
            # Select from the output the lines that contain 'ERROR - []' and
            # 'Unknown type '.
            if 'ERROR - [' in line:
                if not file_name in line:
                    if '/third_party/' in line:
                        add_hide_third_party(build_gn_path)
                    else:
                        print 'UNHANDLED ERROR (Other file): ' + line
                elif 'Unknown type ' in line:
                    unknown_type = line.split('Unknown type ')[1]
                    # Select the main type by removing what follows the dot:
                    # ThumbnailLoader.LoaderType -> ThumbnailLoader.
                    unknown_type = unknown_type.split('.')[0]
                    if not unknown_type in variables_to_import:
                        variables_to_import.append(unknown_type)
                elif 'variable ' in line and ' is undeclared' in line:
                    if ' cr ' in line:  # namespace rewrite.
                        add_namespace_rewrite(build_gn_path)
                        continue
                    unknown_type = line.split('variable ')[1]
                    unknown_type = unknown_type.split(' is undeclared')[0]
                    if not unknown_type in variables_to_import:
                        variables_to_import.append(unknown_type)
                else:
                    print 'UNHANDLED ERROR: ' + line
    return variables_to_import


def find_dependencies(dir_path, build_gn_path, js_file_path,
                      variables_to_import, is_unittest):
    '''Finds dependencies and updates BUILD.gn and JS file to import these
    dependencies.'''
    # Get BUILD.gn and JS file contents.
    build_file_lines = get_file_lines(build_gn_path)
    js_file_lines = get_file_lines(js_file_path)

    file_name = js_file_path.split('/').pop().replace('.js', '')

    # Define the first line of the rule in which the new dependency has
    # to be added.
    rule_first_line = 'js_unittest("%s") {' % (
        file_name) if is_unittest else 'js_library("%s.m") {' % (file_name)

    # Special case: classes that extend "cr.EventTarget".
    index = get_index_substr(js_file_lines, ' extends cr.EventTarget')
    if index >= 0:
        if is_unittest:
            js_file_lines[index] = js_file_lines[index].replace(
                ' extends cr.EventTarget', ' extends EventTarget')
        add_dependency(build_file_lines, rule_first_line, 'deps',
                       '    "//ui/webui/resources/js/cr:event_target.m",')
        add_import_line(js_file_lines, 'NativeEventTarget as EventTarget',
                        'chrome://resources/js/cr/event_target.m.js',
                        is_unittest)

    # General case.
    for variable in variables_to_import:
        # First, handle special cases.

        # "//chrome/test/data/webui:chai_assert".
        out, _ = subprocess.Popen([
            'egrep', '-R', 'export function {}\('.format(variable), '-l',
            './chrome/test/data/webui/chai_assert.js'
        ],
                                  stdout=subprocess.PIPE).communicate()
        if out.rstrip() != '':
            add_dependency(build_file_lines, rule_first_line, 'deps',
                           '    "//chrome/test/data/webui:chai_assert",')
            add_import_line(js_file_lines, variable,
                            'chrome://test/chai_assert.js', is_unittest)
            continue

        # "//ui/webui/resources/js".
        out, _ = subprocess.Popen([
            'egrep', '-R', '\/\* #export \*\/ \w+ {}\W'.format(variable), '-l',
            './ui/webui/resources/js'
        ],
                                  stdout=subprocess.PIPE).communicate()
        path = out.rstrip()
        print path
        if path != '':
            path = path.replace('./', '//').replace('.js', '.m')
            dependency = ':'.join(path.rsplit(
                '/', 1))  # //ui/webui/resources/js:assert.m.
            add_dependency(build_file_lines, rule_first_line, 'deps',
                           '    "{}",'.format(dependency))
            path = path.replace('//ui/webui/', 'chrome://').replace(
                '.m', '.m.js')  # chrome://resources/js/assert.m.js.
            add_import_line(js_file_lines, variable, path, is_unittest)
            continue

        # Look for exported variables.
        out, _ = subprocess.Popen([
            'egrep', '-R', '\/\* #export \*\/ \w+ {}\W'.format(variable), '-l',
            './ui/file_manager/'
        ],
                                  stdout=subprocess.PIPE).communicate()
        path = out.rstrip()
        if path == '':
            # Look for variables exported as wrapped objects
            out, _ = subprocess.Popen([
                'egrep', '-R',
                '\/\* #export \*\/ \{%s\}' % variable, '-l',
                './ui/file_manager/'
            ],
                                      stdout=subprocess.PIPE).communicate()
            path = out.rstrip()
            if path == '':
                continue
        if '\n' in path:
            print '{}: export found in more than 1 file'.format(variable)
            print path
            continue

        # Remove './' at the start.
        path = path[2:]
        relative_path = os.path.relpath(path, start=dir_path).replace(
            '.js', '.m.js')
        if not relative_path.startswith('../'):
            relative_path = './' + relative_path
        add_import_line(js_file_lines, variable, relative_path, is_unittest)
        add_dependency(
            build_file_lines, rule_first_line, 'deps',
            '    "{}",'.format(get_relative_dependency(path, dir_path)))

    # Save BUILD.gn and JS file contents.
    save_file_lines(build_gn_path, build_file_lines)
    save_file_lines(js_file_path, js_file_lines)


def add_js_file_exports(file_lines):
    for i, line in enumerate(file_lines):
        # Export class.
        if line.startswith('class '):
            # Extract class name.
            class_name = line.split(' ')[1]
            # Export class if this class is used or referred to in another js
            # file.
            out, _ = subprocess.Popen([
                'egrep', '-R', '\W{}\W'.format(class_name), '-l',
                './ui/file_manager/'
            ],
                                      stdout=subprocess.PIPE).communicate()
            path = out.rstrip()
            if not '\n' in path:
                print 'Not exporting ' + class_name + ': local class'
                continue
            file_lines[i] = '/* #export */ ' + line
        # Export variable in global namespace.
        elif line.startswith('const ') or line.startswith(
                'let ') or line.startswith('var '):
            # Extract variable name.
            variable_name = line.split(' ')[1]

            # Check if the variable is further defined/extended in the file. If
            # so, export it.
            export_variable = False
            for l in range(len(file_lines)):
                if file_lines[l].startswith(variable_name + '.'):
                    export_variable = True
                    break
            if not export_variable:
                continue

            # Check if export already added.
            if get_index_substr(file_lines, '/* #export */ {%s}' %
                                (variable_name)) >= 0:
                continue

            # Export variable in wrapping object at the end of the file.
            new_lines = '''
// eslint-disable-next-line semi,no-extra-semi
/* #export */ {%s};''' % (variable_name)
            file_lines.extend(new_lines.split('\n'))

            # Check if @suppress already added.
            if get_index_substr(file_lines, ' @suppress {uselessCode}') >= 0:
                continue
            index = get_end_of_file_overview(file_lines)
            if index < 0:
                index = get_end_of_copyright(file_lines)
                if index < 0:
                    index = 0
                index += 1
                new_lines = '''
/**
 * @fileoverview
 * @suppress {uselessCode} Temporary suppress %s.
 */''' % ('because of the line exporting')
                file_lines[index:index] = new_lines.split('\n')
            else:
                while ' */' not in file_lines[index]:
                    index += 1
                new_line = (' * @suppress {uselessCode} Temporary suppress '
                            'because of the line exporting.')
                file_lines.insert(index, new_line)
        # Export function.
        elif line.startswith('function '):
            # Extract function name.
            result = re.search('function (.*)\(', line)
            function_name = result.group(1).strip()

            # Check if the function is used within the current file.
            filtered_file_lines = filter(lambda x: x != line, file_lines)
            if get_index_substr(filtered_file_lines,
                                '{}('.format(function_name)) < 0:
                # The function has to be used outside the file, so has to be
                # exported.
                file_lines[i] = '/* #export */ ' + line


def convert_js_file(js_file_path):
    '''Add exports (/* #export */) and ignore 'use strict'.'''
    file_lines = get_file_lines(js_file_path)

    # Ignore 'use strict'.
    index = get_index_substr(file_lines, "'use strict'")
    if index >= 0:
        file_lines[index] = file_lines[index].replace(
            "'use strict'", "/* #ignore */ 'use strict'")

    # Add exports.
    add_js_file_exports(file_lines)

    # Save file contents.
    save_file_lines(js_file_path, file_lines)


def convert_unittest_file(js_file_path):
    '''Add exports and remove 'use strict'.'''
    file_lines = get_file_lines(js_file_path)

    # Remove 'use strict'.
    index = get_index_substr(file_lines, "'use strict'")
    if index >= 0:
        del file_lines[index]

    # Add exports.
    for i, line in enumerate(file_lines):
        if line.startswith('function setUp()') or line.startswith(
                'function test'):
            file_lines[i] = 'export ' + file_lines[i]

    # Save file contents.
    save_file_lines(js_file_path, file_lines)


def main():
    # Get command line arguments.
    js_file_path = sys.argv[1]
    action = sys.argv[2]
    if not js_file_path.endswith('.js'):
        e = "Argument 1 {} isn't a JS file (expected .js extension)".format(
            js_file_path)
        raise ValueError(e)
    dir_path = os.path.dirname(js_file_path)
    build_gn_path = dir_path + '/BUILD.gn'
    file_name = os.path.basename(js_file_path).replace('.js', '')
    is_unittest = file_name.endswith('_unittest')
    if is_unittest:
        js_file_path = js_file_path.replace('.js', '.m.js')

    if (action == 'generate'):
        if is_unittest:
            update_buid_gn_unittest_dependencies(build_gn_path, js_file_path,
                                                 file_name)
            convert_unittest_file(js_file_path)
        else:
            update_build_gn_dependencies(dir_path, file_name, build_gn_path)
            convert_js_file(js_file_path)
    elif (action == 'parse'):
        compiler_output = sys.argv[3]
        variables_to_import = parse_compiler_output(compiler_output,
                                                    build_gn_path, file_name)
        find_dependencies(dir_path, build_gn_path, js_file_path,
                          variables_to_import, is_unittest)


if __name__ == '__main__':
    print "-----modules.py-----"
    main()
    print "--------------------"

<filename>ui/file_manager/base/tools/modules.py
#!/usr/bin/env python
#
# Copyright (c) 2020 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
import re
import sys
import subprocess


def get_file_lines(file_path):
    '''Returns list of lines from a file. '\n' at the end of lines are
    removed.'''
    with open(file_path, 'r') as f:
        file_lines = f.readlines()
    return [l.rstrip() for l in file_lines]


def save_file_lines(file_path, file_lines):
    '''Saves file_lines in file pointed by `file_path`, '\n' at the end of
    lines are added back.'''
    with open(file_path, 'w') as f:
        for line in file_lines:
            f.write(line + '\n')


def get_relative_dependency(path, dir_path):
    '''Given a file path, returns formatted BUILD.gn dependency.

    Parameters:
        path: file path to format as relative dependency.
        dir_path: directory from which the relative dependency is calculated.

    Returns:
        Formatted dependency.
        The 3 cases illustrated below are handled:
        - ":file_type.m" if returned if file_type.js is in dir_path.
        - "metadata:metadata_model.m" is returned if metadata/metadata_model.js
        is in dir_path.
        - "//ui/file_manager/file_manager/externs:volume_manager.m" is returned if
        ui/file_manager/file_manager/externs is not included in dir_path.
    '''
    split_path = path.split('/')
    file_name = split_path.pop().replace('.js', '.m')
    split_dir_path = dir_path.split('/')
    while (len(split_path) > 0 and len(split_dir_path) > 0
           and split_path[0] == split_dir_path[0]):
        del split_path[0]
        del split_dir_path[0]
    if len(split_dir_path) == 0:
        # The dependency is within dir_path.
        return '/'.join(split_path) + ':' + file_name
    else:
        return '//' + re.sub(r"\/[a-zA-Z0-9_]+\.js", ":", path) + file_name


def get_index_substr(file_lines, substr):
    '''Finds first occurence of `substr` and returns its index in
    `file_lines`.'''
    for i, line in enumerate(file_lines):
        if substr in line:
            return i
    return -1


def get_end_of_copyright(file_lines):
    '''Get index of last line of copyright (after checking that the section
    exists).'''
    index = get_index_substr(file_lines, 'Copyright 20')
    if index < 0:
        return -1
    while index < len(file_lines) and file_lines[index].startswith('// '):
        index += 1
    return index - 1


def get_end_of_file_overview(file_lines):
    '''Get index of last line of file_overview (after checking that the section
    exists).'''
    index = get_index_substr(file_lines, '@fileoverview')
    if index < 0:
        return -1
    while index < len(file_lines) and file_lines[index] != ' */':
        index += 1
    return index


def get_last_index_of_line_starting_with(file_lines, substr):
    '''Get last line starting with the given `substr`.'''
    last_index = -1
    for i, line in enumerate(file_lines):
        if line.startswith(substr):
            last_index = i
    return last_index


def get_end_of_build_imports(file_lines):
    '''In BUILD.gn, gets index of last 'import' line at the beginning of the
    file.'''
    index = get_last_index_of_line_starting_with(file_lines, 'import("//')
    if index < 0:
        return get_end_of_copyright(file_lines)
    return index


def add_js_library(file_lines, file_name, dir_path):
    '''Adds js_library target in BUILD.gn.'''
    i = file_lines.index('js_library("%s") {' % (file_name))

    # Check that the library does exist and hasn't already been converted.
    if i < 0:
        print 'Unable to find js_library for {}'.format(file_name)
        return False
    if 'js_library("%s.m") {' % (file_name) in file_lines:
        print 'js_library for {}.m already added'.format(file_name)
        return False

    # Find the end of the library definition.
    while i < len(file_lines) and file_lines[i] != '}':
        i += 1
    i += 1
    if i == len(file_lines):
        print 'reached end of file'
        return False
    new_lines = '''
js_library("%s.m") {
  sources = [ "$root_gen_dir/%s/%s.m.js" ]

  extra_deps = [ ":modulize" ]
}''' % (file_name, dir_path, file_name)
    file_lines[i:i] = new_lines.split('\n')
    return True


def add_import_line(file_lines, variable, relative_path, is_unittest):
    '''Adds import line (import {...} from '...') in JS file.'''
    # Construct import line.
    import_line = 'import ' if is_unittest else '// #import '

    # Check: existing relative path.
    i = get_index_substr(file_lines, relative_path)
    if i >= 0 and '}' in file_lines[i]:
        if variable + '}' in file_lines[i] or variable + ',' in file_lines[i]:
            return
        split_line = file_lines[i].split('}')
        file_lines[i] = '%s, %s}%s' % (split_line[0], variable, split_line[1])
        return
    import_line += "{%s} from '%s';" % (variable, relative_path)

    if import_line in file_lines:
        return

    # Add clang-format off/on if necessary.
    index = 0
    if '// clang-format off' in file_lines:
        index = file_lines.index('// clang-format off')
    else:
        # Go to the end of copyright and fileoverview.
        index = get_end_of_file_overview(file_lines)
        if index < 0:
            index = get_end_of_copyright(file_lines)
            if (index < 0):
                index = 0
        index += 1
        if len(import_line) > 80:
            index += 1
            file_lines.insert(index, '// clang-format off')

            # Go to the last existing import line.
            last_import_line_index = get_last_index_of_line_starting_with(
                file_lines, 'import ')
            if last_import_line_index >= 0:
                index = last_import_line_index
            else:
                file_lines.insert(index + 1, '')
            file_lines.insert(index + 1, '// clang-format on')
        elif 'import ' not in file_lines[index + 1]:
            file_lines.insert(index, '')

    # Add import line.
    file_lines.insert(index + 1, import_line)


def add_namespace_rewrite(build_gn_path):
    build_file_lines = get_file_lines(build_gn_path)

    # Add import("//ui/webui/resources/js/cr.gni").
    cr_gni = 'import("//ui/webui/resources/js/cr.gni")'
    modulizer_gni = 'import("//ui/webui/resources/tools/js_modulizer.gni")'
    if not cr_gni in build_file_lines:
        if not modulizer_gni in build_file_lines:
            raise ValueError('"js_modulizer.gni" not found')
        index = build_file_lines.index(modulizer_gni)
        build_file_lines.insert(index, cr_gni)

    # Add namespace_rewrites = cr_namespace_rewrites.
    namespace_rewrite = '  namespace_rewrites = cr_namespace_rewrites'
    if not namespace_rewrite in build_file_lines:
        index = get_index_substr(build_file_lines,
                                 'js_modulizer("modulize") {')
        if index < 0:
            print 'No modulize rule found'
            return
        while index < len(build_file_lines) and build_file_lines[index] != '':
            index += 1
        index -= 1
        build_file_lines.insert(index, '')
        build_file_lines.insert(index + 1, namespace_rewrite)

    save_file_lines(build_gn_path, build_file_lines)


def add_hide_third_party(build_gn_path):
    build_file_lines = get_file_lines(build_gn_path)

    hide_third_party = '    "hide_warnings_for=third_party/",'
    prefix_replacement_line = 'browser_resolver_prefix_replacements='
    if not hide_third_party in build_file_lines:
        index = get_index_substr(build_file_lines, prefix_replacement_line)
        if index < 0:
            print 'prefix replacement not found in "js_test_gen_html_modules"'
            return
        build_file_lines.insert(index + 1, hide_third_party)

    save_file_lines(build_gn_path, build_file_lines)


def add_dependency(file_lines, rule_first_line, list_name, dependency_line):
    '''
    Add dependency in BUILD.gn.
    Parameters:
        file_lines: lines of BUILD.gn file.
        rule_first_line: opening line of target to update.
        list_name: name of the dependency list, deps', 'input_files' etc...
        dependency_line: line to add to the dependency list to update.
    '''
    # Find a line that starts with deps.
    if not rule_first_line in file_lines:
        print 'Unable to find ' + rule_first_line
        return False

    # Find index of `list_name`. Get index of 'sources = [' in case `list_name`
    # is not defined.
    rule_index = file_lines.index(rule_first_line)
    sources_index = -1
    insertion_index = -1
    single_line_dependency_list = False

    for i in range(rule_index, len(file_lines)):
        if 'sources = [' in file_lines[i]:
            # Jump to the end of the 'sources' list.
            while not ']' in file_lines[i]:
                i += 1
            sources_index = i
        if '  {} = '.format(list_name) in file_lines[i]:
            # Dependency line found.
            if file_lines[i].endswith(']'):
                single_line_dependency_list = True

            # Go to the end of the dependency list.
            while not ']' in file_lines[i]:
                if dependency_line == file_lines[i]:
                    # Dependency already found.
                    return False
                i += 1
            insertion_index = i
            break
        if file_lines[i] == '}':
            # End of build rule.
            break

    if insertion_index == -1:
        # Add dependency line, after sources if possible.
        index = sources_index + 1 if sources_index > 0 else rule_index + 1

        # Define new list over 2 lines: 'list_name = [\n]'
        file_lines.insert(index, '  {} = ['.format(list_name))
        file_lines.insert(index + 1, '  ]')
        insertion_index = index + 1

    if single_line_dependency_list:
        # Use regex to find characters between [].
        result = re.search('\[(.*)\]', file_lines[insertion_index])
        existing_dependency = result.group(1).strip()
        new_lines = '''\
  {} = [
    {},
  ]'''.format(list_name, existing_dependency)

        # Rewrite single-line dependency list.
        file_lines[insertion_index:insertion_index + 1] = new_lines.split('\n')
        insertion_index += 1

        # Check for already imported dependency after reformatting.
        if file_lines[insertion_index] == dependency_line:
            return False

        # If there was no existing dependency, remove appropriate line.
        if existing_dependency == '':
            del file_lines[insertion_index]

    # Insert dependency.
    file_lines.insert(insertion_index, dependency_line)
    return True


def update_build_gn_dependencies(dir_path, file_name, build_gn_path):
    print 'Updating BUILD.gn dependencies for ' + file_name

    # Get file contents.
    file_lines = get_file_lines(build_gn_path)

    # Edit file with modules-related targets.
    import_gni = 'import("//ui/webui/resources/tools/js_modulizer.gni")'
    if not import_gni in file_lines:
        index = get_end_of_build_imports(file_lines) + 1
        file_lines.insert(index, import_gni)
        new_lines = '''
js_modulizer("modulize") {
  input_files = [
  ]
}'''
        file_lines.extend(new_lines.split('\n'))
        if not add_dependency(file_lines, 'group("closure_compile") {', 'deps',
                              '    ":closure_compile_jsmodules",'):
            return

        # Add closure_compile_jsmodules rule.
        index = get_index_substr(file_lines,
                                 'js_type_check("closure_compile_module") {')
        if index < 0:
            print 'js_type_check("closure_compile_module") not found'
            return
        new_lines = '''\
js_type_check("closure_compile_jsmodules") {
  deps = [
  ]
}
'''
        file_lines[index:index] = new_lines.split('\n')
    if not add_js_library(file_lines, file_name, dir_path):
        return

    # Add closure dependency.
    if not add_dependency(
            file_lines, 'js_type_check("closure_compile_jsmodules") {', 'deps',
            '    ":{}.m",'.format(file_name)):
        return

    # Add 'modulize' dependency.
    if not add_dependency(file_lines, 'js_modulizer("modulize") {',
                          'input_files', '    "{}.js",'.format(file_name)):
        return

    # Save file contents.
    save_file_lines(build_gn_path, file_lines)


def update_buid_gn_unittest_dependencies(build_gn_path, js_file_path,
                                         file_name):
    '''
    Rename _unittest.js to _unittest.m.js.
    Update test URL in file_manager_jstest.cc.
    Update BUILD.gn rules.
    It is assumed that if we're converting [file]_unittest.js, [file].js has
    been converted already.

    Parameters:
        build_gn_path: Path of BUILD.gn file used by the file be converted.
        js_file_path: Path of the file to be converted
                        (ui/file_manager/.../..._unittest.m.js)
        file_name: Name of the file to be converted (..._unittest)
    '''
    print 'Updating BUILD.gn dependencies for ' + file_name

    main_build_path = 'ui/file_manager/BUILD.gn'
    file_manager_jstest_path = ('chrome/browser/chromeos/file_manager/'
                                'file_manager_jstest.cc')

    # Get file contents.
    build_file_lines = get_file_lines(build_gn_path)
    main_build_file_lines = get_file_lines(main_build_path)
    file_manager_jstest_lines = get_file_lines(file_manager_jstest_path)

    # Rename _unittest.js to _unittest.m.js.
    original_js_file_path = js_file_path.replace('.m.js', '.js')
    if not os.path.isfile(js_file_path):
        if not os.path.isfile(original_js_file_path):
            e = 'Could not find neither file {} or {}'.format(
                js_file_path, original_js_file_path)
            raise ValueError(e)
        os.rename(original_js_file_path, js_file_path)

    # Update chrome/browser/chromeos/file_manager/file_manager_jstest.cc
    # by updating RunTestURL for the unittest being converted.
    # [...]_unittest_gen.html -> [...]_unittest.m_gen.html.
    index = get_index_substr(file_manager_jstest_lines, file_name + '.m')
    if index < 0:
        index = get_index_substr(file_manager_jstest_lines, file_name)
        if index < 0:
            print 'file_manager_jstest.cc: {} not found'.format(file_name)
        file_manager_jstest_lines[index] = file_manager_jstest_lines[
            index].replace(file_name, file_name + '.m')

    # Add "js_test_gen_html_modules" target.
    build_rule = 'js_test_gen_html("js_test_gen_html_modules") {'
    if not build_rule in build_file_lines:
        if 'js_test_gen_html("js_test_gen_html") {' in build_file_lines:
            index = build_file_lines.index(
                'js_test_gen_html("js_test_gen_html") {') - 1
            new_lines = r'''
js_test_gen_html("js_test_gen_html_modules") {
  deps = [
  ]
  js_module = true

  closure_flags =
      strict_error_checking_closure_args + [
        "js_module_root=./gen/ui",
        "js_module_root=../../ui",
        "browser_resolver_prefix_replacements=\"chrome://test/=./\"",
      ]
}'''
            build_file_lines[index:index] = new_lines.split('\n')
        else:
            e = '"js_test_gen_html" build target not found on {}'.format(
                build_gn_path)
            raise ValueError(e)

    # Add ":js_test_gen_html_modules_type_check_auto", in closure_compile deps.
    index = get_index_substr(build_file_lines,
                             ':js_test_gen_html_modules_type_check_auto')
    if index < 0:
        index = get_index_substr(build_file_lines,
                                 ':js_test_gen_html_type_check_auto')
        if index < 0:
            e = '"js_test_gen_html_type_check_auto" dependency not found'
            raise ValueError(e)
        else:
            new_line = '    ":js_test_gen_html_modules_type_check_auto",'
            build_file_lines.insert(index, new_line)

    # In BUILD.gn, update js_unittest target to .m
    js_unittest = 'js_unittest("%s") {' % (file_name)
    js_unittest_m = 'js_unittest("%s.m") {' % (file_name)
    if js_unittest in build_file_lines:
        index = build_file_lines.index(js_unittest)
        build_file_lines[index] = js_unittest_m
        # Remove dependencies.
        while index < len(
                build_file_lines) and not 'deps =' in build_file_lines[index]:
            index += 1
        if ']' in build_file_lines[index]:
            build_file_lines[index] = '  deps = ['
            build_file_lines.insert(index, '  ]')
        else:
            index += 1
            while index < len(
                    build_file_lines) and not ']' in build_file_lines[index]:
                del build_file_lines[index]
    else:
        if not js_unittest_m in build_file_lines:
            print 'Unable to find "{}" target'.format(file_name)

    # Move unittest dependency from js_test_gen_html to
    # js_test_gen_html_modules.
    index = get_index_substr(build_file_lines, '":{}",'.format(file_name))
    if index >= 0:
        del build_file_lines[index]
    else:
        single_line_deps = '  deps = [ ":{}" ]'.format(file_name)
        if single_line_deps in build_file_lines:
            index = build_file_lines.index(single_line_deps)
            build_file_lines[index] = '  deps = []'

    add_dependency(build_file_lines,
                   'js_test_gen_html("js_test_gen_html_modules") {', 'deps',
                   '    ":{}.m",'.format(file_name))
    # Add dependency in ui/file_manager/BUILD.gn.
    dir_path = os.path.dirname(js_file_path)
    rel_dir_path = '/'.join(dir_path.split('/')[2:])
    dependency_line = '    "{}:js_test_gen_html_modules",'.format(rel_dir_path)
    if not dependency_line in main_build_file_lines:
        if not add_dependency(main_build_file_lines,
                              'group("unit_test_data") {', 'deps',
                              dependency_line):
            print 'unable to update ui/file_manager/BUILD.gn'

    # Save file contents.
    save_file_lines(build_gn_path, build_file_lines)
    save_file_lines(main_build_path, main_build_file_lines)
    save_file_lines(file_manager_jstest_path, file_manager_jstest_lines)


def parse_compiler_output(compiler_output, build_gn_path, file_name):
    '''Parse closure compile output and return list of token that are
    undefined/undeclared (no duplicate).'''
    print "Parsing closure compiler output"
    with open(compiler_output, 'r') as f:
        # Retrieve from the closure compile output the types that need to be
        # imported.
        variables_to_import = []
        for line in f:
            line = line.rstrip()
            # Select from the output the lines that contain 'ERROR - []' and
            # 'Unknown type '.
            if 'ERROR - [' in line:
                if not file_name in line:
                    if '/third_party/' in line:
                        add_hide_third_party(build_gn_path)
                    else:
                        print 'UNHANDLED ERROR (Other file): ' + line
                elif 'Unknown type ' in line:
                    unknown_type = line.split('Unknown type ')[1]
                    # Select the main type by removing what follows the dot:
                    # ThumbnailLoader.LoaderType -> ThumbnailLoader.
                    unknown_type = unknown_type.split('.')[0]
                    if not unknown_type in variables_to_import:
                        variables_to_import.append(unknown_type)
                elif 'variable ' in line and ' is undeclared' in line:
                    if ' cr ' in line:  # namespace rewrite.
                        add_namespace_rewrite(build_gn_path)
                        continue
                    unknown_type = line.split('variable ')[1]
                    unknown_type = unknown_type.split(' is undeclared')[0]
                    if not unknown_type in variables_to_import:
                        variables_to_import.append(unknown_type)
                else:
                    print 'UNHANDLED ERROR: ' + line
    return variables_to_import


def find_dependencies(dir_path, build_gn_path, js_file_path,
                      variables_to_import, is_unittest):
    '''Finds dependencies and updates BUILD.gn and JS file to import these
    dependencies.'''
    # Get BUILD.gn and JS file contents.
    build_file_lines = get_file_lines(build_gn_path)
    js_file_lines = get_file_lines(js_file_path)

    file_name = js_file_path.split('/').pop().replace('.js', '')

    # Define the first line of the rule in which the new dependency has
    # to be added.
    rule_first_line = 'js_unittest("%s") {' % (
        file_name) if is_unittest else 'js_library("%s.m") {' % (file_name)

    # Special case: classes that extend "cr.EventTarget".
    index = get_index_substr(js_file_lines, ' extends cr.EventTarget')
    if index >= 0:
        if is_unittest:
            js_file_lines[index] = js_file_lines[index].replace(
                ' extends cr.EventTarget', ' extends EventTarget')
        add_dependency(build_file_lines, rule_first_line, 'deps',
                       '    "//ui/webui/resources/js/cr:event_target.m",')
        add_import_line(js_file_lines, 'NativeEventTarget as EventTarget',
                        'chrome://resources/js/cr/event_target.m.js',
                        is_unittest)

    # General case.
    for variable in variables_to_import:
        # First, handle special cases.

        # "//chrome/test/data/webui:chai_assert".
        out, _ = subprocess.Popen([
            'egrep', '-R', 'export function {}\('.format(variable), '-l',
            './chrome/test/data/webui/chai_assert.js'
        ],
                                  stdout=subprocess.PIPE).communicate()
        if out.rstrip() != '':
            add_dependency(build_file_lines, rule_first_line, 'deps',
                           '    "//chrome/test/data/webui:chai_assert",')
            add_import_line(js_file_lines, variable,
                            'chrome://test/chai_assert.js', is_unittest)
            continue

        # "//ui/webui/resources/js".
        out, _ = subprocess.Popen([
            'egrep', '-R', '\/\* #export \*\/ \w+ {}\W'.format(variable), '-l',
            './ui/webui/resources/js'
        ],
                                  stdout=subprocess.PIPE).communicate()
        path = out.rstrip()
        print path
        if path != '':
            path = path.replace('./', '//').replace('.js', '.m')
            dependency = ':'.join(path.rsplit(
                '/', 1))  # //ui/webui/resources/js:assert.m.
            add_dependency(build_file_lines, rule_first_line, 'deps',
                           '    "{}",'.format(dependency))
            path = path.replace('//ui/webui/', 'chrome://').replace(
                '.m', '.m.js')  # chrome://resources/js/assert.m.js.
            add_import_line(js_file_lines, variable, path, is_unittest)
            continue

        # Look for exported variables.
        out, _ = subprocess.Popen([
            'egrep', '-R', '\/\* #export \*\/ \w+ {}\W'.format(variable), '-l',
            './ui/file_manager/'
        ],
                                  stdout=subprocess.PIPE).communicate()
        path = out.rstrip()
        if path == '':
            # Look for variables exported as wrapped objects
            out, _ = subprocess.Popen([
                'egrep', '-R',
                '\/\* #export \*\/ \{%s\}' % variable, '-l',
                './ui/file_manager/'
            ],
                                      stdout=subprocess.PIPE).communicate()
            path = out.rstrip()
            if path == '':
                continue
        if '\n' in path:
            print '{}: export found in more than 1 file'.format(variable)
            print path
            continue

        # Remove './' at the start.
        path = path[2:]
        relative_path = os.path.relpath(path, start=dir_path).replace(
            '.js', '.m.js')
        if not relative_path.startswith('../'):
            relative_path = './' + relative_path
        add_import_line(js_file_lines, variable, relative_path, is_unittest)
        add_dependency(
            build_file_lines, rule_first_line, 'deps',
            '    "{}",'.format(get_relative_dependency(path, dir_path)))

    # Save BUILD.gn and JS file contents.
    save_file_lines(build_gn_path, build_file_lines)
    save_file_lines(js_file_path, js_file_lines)


def add_js_file_exports(file_lines):
    for i, line in enumerate(file_lines):
        # Export class.
        if line.startswith('class '):
            # Extract class name.
            class_name = line.split(' ')[1]
            # Export class if this class is used or referred to in another js
            # file.
            out, _ = subprocess.Popen([
                'egrep', '-R', '\W{}\W'.format(class_name), '-l',
                './ui/file_manager/'
            ],
                                      stdout=subprocess.PIPE).communicate()
            path = out.rstrip()
            if not '\n' in path:
                print 'Not exporting ' + class_name + ': local class'
                continue
            file_lines[i] = '/* #export */ ' + line
        # Export variable in global namespace.
        elif line.startswith('const ') or line.startswith(
                'let ') or line.startswith('var '):
            # Extract variable name.
            variable_name = line.split(' ')[1]

            # Check if the variable is further defined/extended in the file. If
            # so, export it.
            export_variable = False
            for l in range(len(file_lines)):
                if file_lines[l].startswith(variable_name + '.'):
                    export_variable = True
                    break
            if not export_variable:
                continue

            # Check if export already added.
            if get_index_substr(file_lines, '/* #export */ {%s}' %
                                (variable_name)) >= 0:
                continue

            # Export variable in wrapping object at the end of the file.
            new_lines = '''
// eslint-disable-next-line semi,no-extra-semi
/* #export */ {%s};''' % (variable_name)
            file_lines.extend(new_lines.split('\n'))

            # Check if @suppress already added.
            if get_index_substr(file_lines, ' @suppress {uselessCode}') >= 0:
                continue
            index = get_end_of_file_overview(file_lines)
            if index < 0:
                index = get_end_of_copyright(file_lines)
                if index < 0:
                    index = 0
                index += 1
                new_lines = '''
/**
 * @fileoverview
 * @suppress {uselessCode} Temporary suppress %s.
 */''' % ('because of the line exporting')
                file_lines[index:index] = new_lines.split('\n')
            else:
                while ' */' not in file_lines[index]:
                    index += 1
                new_line = (' * @suppress {uselessCode} Temporary suppress '
                            'because of the line exporting.')
                file_lines.insert(index, new_line)
        # Export function.
        elif line.startswith('function '):
            # Extract function name.
            result = re.search('function (.*)\(', line)
            function_name = result.group(1).strip()

            # Check if the function is used within the current file.
            filtered_file_lines = filter(lambda x: x != line, file_lines)
            if get_index_substr(filtered_file_lines,
                                '{}('.format(function_name)) < 0:
                # The function has to be used outside the file, so has to be
                # exported.
                file_lines[i] = '/* #export */ ' + line


def convert_js_file(js_file_path):
    '''Add exports (/* #export */) and ignore 'use strict'.'''
    file_lines = get_file_lines(js_file_path)

    # Ignore 'use strict'.
    index = get_index_substr(file_lines, "'use strict'")
    if index >= 0:
        file_lines[index] = file_lines[index].replace(
            "'use strict'", "/* #ignore */ 'use strict'")

    # Add exports.
    add_js_file_exports(file_lines)

    # Save file contents.
    save_file_lines(js_file_path, file_lines)


def convert_unittest_file(js_file_path):
    '''Add exports and remove 'use strict'.'''
    file_lines = get_file_lines(js_file_path)

    # Remove 'use strict'.
    index = get_index_substr(file_lines, "'use strict'")
    if index >= 0:
        del file_lines[index]

    # Add exports.
    for i, line in enumerate(file_lines):
        if line.startswith('function setUp()') or line.startswith(
                'function test'):
            file_lines[i] = 'export ' + file_lines[i]

    # Save file contents.
    save_file_lines(js_file_path, file_lines)


def main():
    # Get command line arguments.
    js_file_path = sys.argv[1]
    action = sys.argv[2]
    if not js_file_path.endswith('.js'):
        e = "Argument 1 {} isn't a JS file (expected .js extension)".format(
            js_file_path)
        raise ValueError(e)
    dir_path = os.path.dirname(js_file_path)
    build_gn_path = dir_path + '/BUILD.gn'
    file_name = os.path.basename(js_file_path).replace('.js', '')
    is_unittest = file_name.endswith('_unittest')
    if is_unittest:
        js_file_path = js_file_path.replace('.js', '.m.js')

    if (action == 'generate'):
        if is_unittest:
            update_buid_gn_unittest_dependencies(build_gn_path, js_file_path,
                                                 file_name)
            convert_unittest_file(js_file_path)
        else:
            update_build_gn_dependencies(dir_path, file_name, build_gn_path)
            convert_js_file(js_file_path)
    elif (action == 'parse'):
        compiler_output = sys.argv[3]
        variables_to_import = parse_compiler_output(compiler_output,
                                                    build_gn_path, file_name)
        find_dependencies(dir_path, build_gn_path, js_file_path,
                          variables_to_import, is_unittest)


if __name__ == '__main__':
    print "-----modules.py-----"
    main()
    print "--------------------"

#!/usr/bin/env python # # Copyright (c) 2020 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. Returns list of lines from a file. '\n' at the end of lines are     removed. Saves file_lines in file pointed by `file_path`, '\n' at the end of     lines are added back. Given a file path, returns formatted BUILD.gn dependency.      Parameters:         path: file path to format as relative dependency.         dir_path: directory from which the relative dependency is calculated.      Returns:         Formatted dependency.         The 3 cases illustrated below are handled:         - ":file_type.m" if returned if file_type.js is in dir_path.         - "metadata:metadata_model.m" is returned if metadata/metadata_model.js         is in dir_path.         - "//ui/file_manager/file_manager/externs:volume_manager.m" is returned if         ui/file_manager/file_manager/externs is not included in dir_path. # The dependency is within dir_path. Finds first occurence of `substr` and returns its index in     `file_lines`. Get index of last line of copyright (after checking that the section     exists). Get index of last line of file_overview (after checking that the section     exists). Get last line starting with the given `substr`. In BUILD.gn, gets index of last 'import' line at the beginning of the     file. Adds js_library target in BUILD.gn. # Check that the library does exist and hasn't already been converted. # Find the end of the library definition. js_library("%s.m") {   sources = [ "$root_gen_dir/%s/%s.m.js" ]    extra_deps = [ ":modulize" ] } Adds import line (import {...} from '...') in JS file. # Construct import line. #import ' # Check: existing relative path. # Add clang-format off/on if necessary. # Go to the end of copyright and fileoverview. # Go to the last existing import line. # Add import line. # Add import("//ui/webui/resources/js/cr.gni"). # Add namespace_rewrites = cr_namespace_rewrites. Add dependency in BUILD.gn.     Parameters:         file_lines: lines of BUILD.gn file.         rule_first_line: opening line of target to update.         list_name: name of the dependency list, deps', 'input_files' etc...         dependency_line: line to add to the dependency list to update. # Find a line that starts with deps. # Find index of `list_name`. Get index of 'sources = [' in case `list_name` # is not defined. # Jump to the end of the 'sources' list. # Dependency line found. # Go to the end of the dependency list. # Dependency already found. # End of build rule. # Add dependency line, after sources if possible. # Define new list over 2 lines: 'list_name = [\n]' # Use regex to find characters between []. \   {} = [     {},   ] # Rewrite single-line dependency list. # Check for already imported dependency after reformatting. # If there was no existing dependency, remove appropriate line. # Insert dependency. # Get file contents. # Edit file with modules-related targets. js_modulizer("modulize") {   input_files = [   ] } # Add closure_compile_jsmodules rule. \ js_type_check("closure_compile_jsmodules") {   deps = [   ] } # Add closure dependency. # Add 'modulize' dependency. # Save file contents. Rename _unittest.js to _unittest.m.js.     Update test URL in file_manager_jstest.cc.     Update BUILD.gn rules.     It is assumed that if we're converting [file]_unittest.js, [file].js has     been converted already.      Parameters:         build_gn_path: Path of BUILD.gn file used by the file be converted.         js_file_path: Path of the file to be converted                         (ui/file_manager/.../..._unittest.m.js)         file_name: Name of the file to be converted (..._unittest) # Get file contents. # Rename _unittest.js to _unittest.m.js. # Update chrome/browser/chromeos/file_manager/file_manager_jstest.cc # by updating RunTestURL for the unittest being converted. # [...]_unittest_gen.html -> [...]_unittest.m_gen.html. # Add "js_test_gen_html_modules" target. js_test_gen_html("js_test_gen_html_modules") {   deps = [   ]   js_module = true    closure_flags =       strict_error_checking_closure_args + [         "js_module_root=./gen/ui",         "js_module_root=../../ui",         "browser_resolver_prefix_replacements=\"chrome://test/=./\"",       ] } # Add ":js_test_gen_html_modules_type_check_auto", in closure_compile deps. # In BUILD.gn, update js_unittest target to .m # Remove dependencies. # Move unittest dependency from js_test_gen_html to # js_test_gen_html_modules. # Add dependency in ui/file_manager/BUILD.gn. # Save file contents. Parse closure compile output and return list of token that are     undefined/undeclared (no duplicate). # Retrieve from the closure compile output the types that need to be # imported. # Select from the output the lines that contain 'ERROR - []' and # 'Unknown type '. # Select the main type by removing what follows the dot: # ThumbnailLoader.LoaderType -> ThumbnailLoader. # namespace rewrite. Finds dependencies and updates BUILD.gn and JS file to import these     dependencies. # Get BUILD.gn and JS file contents. # Define the first line of the rule in which the new dependency has # to be added. # Special case: classes that extend "cr.EventTarget". # General case. # First, handle special cases. # "//chrome/test/data/webui:chai_assert". # "//ui/webui/resources/js". #export \*\/ \w+ {}\W'.format(variable), '-l', # //ui/webui/resources/js:assert.m. # chrome://resources/js/assert.m.js. # Look for exported variables. #export \*\/ \w+ {}\W'.format(variable), '-l', # Look for variables exported as wrapped objects #export \*\/ \{%s\}' % variable, '-l', # Remove './' at the start. # Save BUILD.gn and JS file contents. # Export class. # Extract class name. # Export class if this class is used or referred to in another js # file. #export */ ' + line # Export variable in global namespace. # Extract variable name. # Check if the variable is further defined/extended in the file. If # so, export it. # Check if export already added. #export */ {%s}' % # Export variable in wrapping object at the end of the file. // eslint-disable-next-line semi,no-extra-semi /* #export */ {%s}; # Check if @suppress already added. /**  * @fileoverview  * @suppress {uselessCode} Temporary suppress %s.  */ # Export function. # Extract function name. # Check if the function is used within the current file. # The function has to be used outside the file, so has to be # exported. #export */ ' + line Add exports (/* #export */) and ignore 'use strict'. # Ignore 'use strict'. #ignore */ 'use strict'") # Add exports. # Save file contents. Add exports and remove 'use strict'. # Remove 'use strict'. # Add exports. # Save file contents. # Get command line arguments.