chromium/tools/typescript/validate_tsconfig.py

# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Overview: ts_library() supports only a small subset of all possible tsconfig
# configurations. Some options cannot be used in general, and some options
# should only be set through the ts_library() gn args corresponding to them. In
# the latter case, there are requirements for how some of these args can be
# configured. This file contains validation logic for tsconfig files and the gn
# args corresponding to config options to limit the possibility of unsupported
# configurations proliferating in the codebase.

import os
import pathlib

from path_utils import isInAshFolder, getTargetPath

_CWD = os.getcwd().replace('\\', '/')
_HERE_DIR = os.path.dirname(__file__)
_SRC_DIR = os.path.normpath(os.path.join(_HERE_DIR, '..',
                                         '..')).replace('\\', '/')

# Options configured by the ts_library should not be set separately.
_tsconfig_compiler_options_mappings = {
    'composite': 'composite=true',
    'declaration': 'composite=true',
    'inlineSourceMap': 'enable_source_maps=true',
    'inlineSources': 'enable_source_maps=true',
    'outDir': 'out_dir',
    'paths': 'path_mappings',
    'rootDir': 'root_dir',
    'tsBuildInfoFile': 'composite=true',
}

# Allowed options within tsconfig_base.json
_allowed_config_options = [
    'extends',
    'compilerOptions',
]

# Allowed compilerOptions
_allowed_compiler_options = [
    'allowUmdGlobalAccess',
    'isolatedModules',
    'lib',
    'noPropertyAccessFromIndexSignature',
    'noUncheckedIndexedAccess',
    'noUnusedLocals',
    'skipLibCheck',
    'strictPropertyInitialization',
    'typeRoots',
    'types',
    'useDefineForClassFields',
]


def validateTsconfigJson(tsconfig, tsconfig_file, is_base_tsconfig):
  # Special exception for material_web_components, which uses ts_library()
  # in an unsupported way.
  if 'third_party/material_web_components/tsconfig_base.json' in tsconfig_file:
    return True, None

  # TODO(b/267329383): Migrate A11y to TypeScript. Accessibility code has
  # different requirements for the migration because of this we need both
  # allowjs and a custom tsconfig.
  if 'accessibility/tsconfig.base.json' in tsconfig_file:
    return True, None

  if not is_base_tsconfig:
    for param in tsconfig.keys():
      if param not in _allowed_config_options:
        return False, f'Invalid |{param}| option detected in ' + \
            f'{tsconfig_file}.Only |extends| and |compilerOptions| may ' + \
            'be specified.'

  if 'compilerOptions' in tsconfig:
    for param in _tsconfig_compiler_options_mappings.keys():
      if param in tsconfig['compilerOptions']:
        tslibrary_flag = _tsconfig_compiler_options_mappings[param]
        return False, f'Invalid |{param}| flag detected in {tsconfig_file}.' + \
            f' Use the dedicated |{tslibrary_flag}| attribute in '+ \
            'ts_library() instead.'

    if 'ui/file_manager' in tsconfig_file:
      # File manager uses ts_library() in an unsupported way. Just return true
      # here for this special case.
      return True, None

    if not is_base_tsconfig:
      for input_param in tsconfig['compilerOptions'].keys():
        if input_param not in _allowed_compiler_options:
          return False, f'Disallowed |{input_param}| flag detected in '+ \
              f'{tsconfig_file}.'

  return True, None


# Note 1: DO NOT add any directories here corresponding to newly added or
# existing TypeScript code. Instead use TypeScript, which is a requirement.
# Note 2: Only add a directory here if you are in the process of migrating
# legacy JS code to TS. Any new entries here should be accompanied by a bug
# tracking the TS migration.
def validateJavaScriptAllowed(source_dir, out_dir, is_ios):
  # Special case for iOS, which sets the root src/ directory as the source
  # directory for the ts_library() call, see
  # ios/web/public/js_messaging/compile_ts.gni.
  # We don't want to generally allow allow_js anywhere in src/ so check the
  # output directory against the standard ios directories instead. This is a
  # really broad check so also use the platform to make sure this is not abused
  # elsewhere; the iOS use case of using allowJs broadly is not supported.
  if (is_ios and '/ios/' in out_dir):
    return True, None

  # Anything in these ChromeOS-specific directories is allowed to use allow_js.
  # TODO (rbpotter): If possible, standardize the build setup in some of these
  # folders such that they can be more accurately specified in the list below.
  ash_directories = [
      'ash/webui/camera_app_ui/',
      'ash/webui/color_internals/',
      'ash/webui/common/resources/',
      'ash/webui/diagnostics_ui/',
      'ash/webui/file_manager/resources/labs/',
      # TODO(b/314827247): Migrate media_app_ui to TypeScript and remove
      # exception.
      'ash/webui/media_app_ui/',
      # TODO(b/313562946): Migrate help_app_ui mojo pipeline to TypeScript and
      # remove.
      'ash/webui/help_app_ui/',
      # TODO(b/315002705): Migrate shimless_rma to TypeScript and remove
      # exception.
      'ash/webui/shimless_rma/',
      # TODO(b/267329383): Migrate A11y to TypeScript.
      'chrome/browser/resources/chromeos/accessibility',
      'ui/file_manager/',
  ]
  for directory in ash_directories:
    if directory in source_dir:
      return True, None

  # Specific exceptions for directories that are still migrating to TS.
  migrating_directories = [
      # TODO(crbug.com/40848285): Migrate bluetooth-internals to TypeScript and
      # remove exception.
      'chrome/browser/resources/bluetooth_internals',
      'chrome/browser/resources/chromeos/accessibility',
      # TODO(crbug.com/41484340): Migrate to TypeScript.
      'chrome/browser/resources/device_log',
      'chrome/test/data/webui',
      # TODO(crbug.com/40848285): Migrate bluetooth-internals to TypeScript and
      # remove exception.
      'chrome/test/data/webui/bluetooth_internals',
      'chrome/test/data/webui/chromeos',
      'chrome/test/data/webui/chromeos/ash_common',
      # TODO(b/245336251): Migrate diagnostics app tests to Typescript and
      # remove exception.
      'chrome/test/data/webui/chromeos/diagnostics',
      # TODO(b/315002705): Migrate shimless rma app tests to Typescript and
      # remove exception.
      'chrome/test/data/webui/chromeos/shimless_rma',
      'chrome/test/data/webui/cr_components/chromeos',
      'chrome/test/data/webui/nearby_share',
      'components/policy/resources/webui',
      'ui/webui/resources/js',
      'ui/webui/resources/mojo',

      # TODO(crbug.com/40280699) : Migrate to TypeScript.
      'chrome/test/data/webui/media_internals',
      'content/browser/resources/media',

      # TODO(b/274059668): Migrate OOBE to TypeScript.
      'chrome/browser/resources/chromeos/login',
  ]
  for directory in migrating_directories:
    if (source_dir.endswith(directory)
        or source_dir.endswith(directory + '/preprocessed')):
      return True, None

  return False, 'Invalid JS file detected for input directory ' + \
      f'{source_dir} and output directory {out_dir}, all new ' + \
      'code should be added in TypeScript.'


def isMappingAllowed(is_ash_target, target_path, mapping_path):
  if is_ash_target:
    return True

  return not isInAshFolder(mapping_path) or target_path in exceptions


# TODO (https://www.crbug.com/1412158): Remove all exceptions below and this
# function; these build targets rely on implicitly unmapped dependencies.
def isUnsupportedJsTarget(gen_dir, root_gen_dir):
  target_path = getTargetPath(gen_dir, root_gen_dir)
  exceptions = [
      'ash/webui/color_internals/resources',
      'chrome/browser/resources/chromeos/accessibility/select_to_speak',
  ]
  return target_path in exceptions


# |root_dir| shouldn't refer to any parent directories. Specifically it should
# be either:
#   - within the folder tree starting at the ts_library() target's location
#   - within the folder tree starting at the ts_library() target's corresponding
#     target_gen_dir location.
def validateRootDir(root_dir, gen_dir, root_gen_dir, is_ios):
  root_gen_dir_from_build = os.path.normpath(os.path.join(
      gen_dir, root_gen_dir)).replace('\\', '/')
  target_path = os.path.relpath(gen_dir,
                                root_gen_dir_from_build).replace('\\', '/')

  # Broadly special casing ios/ for now, since compile_ts.gni relies on
  # unsupported behavior of setting the root_dir to src/.
  # TODO (https://www.crbug.com/1412158): Make iOS TypeScript build tools use
  # ts_library in a supported way, or change them to not rely on ts_library.
  if (is_ios and 'ios' in pathlib.Path(target_path).parts):
    return True, None

  # Legacy cases supported for backward-compatibility. Do not add new targets
  # here. The existing exceptions should be removed over time.
  exceptions = [
      # ChromeOS cases
      'ash/webui/color_internals/mojom',
  ]

  if target_path in exceptions:
    return True, None

  target_path_src = os.path.relpath(os.path.join(_SRC_DIR, target_path),
                                    _CWD).replace('\\', '/')
  root_path_from_gen = os.path.relpath(root_dir,
                                       root_gen_dir_from_build).replace(
                                           '\\', '/')
  root_path_from_src = os.path.relpath(os.path.join(_CWD, root_dir),
                                       _SRC_DIR).replace('\\', '/')

  if (root_path_from_gen.startswith(target_path)
      or root_path_from_src.startswith(target_path)):
    return True, None

  return False, f'Error: root_dir ({root_dir}) should be within {gen_dir} ' + \
      f'or {target_path_src}.'


def validateDefinitionDeps(definitions_files, target_path, gen_dir,
                           root_gen_dir, definitions):
  # Root gen dir relative to the current working directory (essentially 'gen')
  gen_dir_from_build = os.path.normpath(os.path.join(gen_dir,
                                                     root_gen_dir)).replace(
                                                         '\\', '/')

  def getPathFromCwd(exception):
    return os.path.relpath(os.path.join(_SRC_DIR, exception),
                           _CWD).replace('\\', '/')

  # TODO(https://crbug.com/326005022): Determine if the following are actually
  # safe for computation of gn input values.
  exceptions_list = [
      'third_party/material_web_components/',
      'third_party/node/node_modules/',
      'third_party/polymer/v3_0/',
      'tools/typescript/tests/',
  ]
  exceptions = [getPathFromCwd(e) for e in exceptions_list]
  definitions_normalized = [d.replace('\\', '/') for d in definitions]

  missing_inputs = []
  for f in definitions_files:
    # File path relative to the current working directory.
    f_from_cwd = os.path.relpath(f, _CWD).replace('\\', '/')
    is_gen_file = f_from_cwd.startswith(gen_dir_from_build)
    f_from_gen = os.path.relpath(f, gen_dir).replace('\\', '/')
    if not is_gen_file and f_from_gen not in definitions_normalized and \
        not any(f_from_cwd.startswith(exception) for exception in exceptions):
      missing_inputs.append(
          os.path.relpath(f_from_cwd, _SRC_DIR).replace('\\', '/'))

  if not missing_inputs:
    return True, None

  errorMessage = 'Undeclared dependencies to definition files encountered ' + \
                 f'while building {target_path}. Please list the following ' + \
                 'file(s) in |definitions|:\n'
  for missing_input in missing_inputs:
    errorMessage += f'//{missing_input}\n'

  return False, errorMessage