chromium/third_party/google-closure-library/scripts/check_closure_oss.js

/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview A utility to check that all files adhere to Google open-source
 * standards.
 */

const {promises: fs} = require('fs');
const {exec} = require('child_process');
const path = require('path');
const {promisify} = require('util');

const IGNORED_EXTENSIONS =
    ['.gif', '.jpg', '.png', '.txt', '.data', '.json', '.enc', '.exe', '.yml'];
const IGNORED_FILES = [
  'AUTHORS',
  'CONTRIBUTING',
  'LICENSE',
  'README.md',
  'WORKSPACE',
  '.npmignore',
  'closure-deps/AUTHORS',
  'closure-deps/CONTRIBUTING',
  'closure-deps/LICENSE',
  'closure-deps/README.md',
  'closure/known_issues/testdata/closure_library_warnings.txt',
  'closure/goog/BUILD.bazel',
];

const APACHE_LICENSE_REGEXES =
    [/apache license.*2\.0/i, /SPDX-License-Identifier: Apache-2.0/];
const CC_BY_LICENSE_REGEXES = [
  /Documentation licensed under CC BY 4\.0/,
  /License available at https:\/\/creativecommons\.org\/licenses\/by\/4\.0\//
];
const CONFORMANCE_ALLOWLIST_REGEX = /whitelist: \'(?!closurei\/)/i; // nocheck
const CLOSURE_AUTHORS_COPYRIGHT_REGEX =
    /Copyright( 20\d\d)? The Closure Library Authors/;
const DOC_FILE_REGEX = /(?:^doc\/.*\.html|\.md)$/;
const GOOGLE_COPYRIGHT_REGEX = /copyright.{0,70}[^@]google/i;

const CONFORMANCE_PROTO_PATH = 'closure/goog/conformance_proto.txt';
const REPOSITORY_PATH = path.resolve(`${__dirname}/..`);

const execP = promisify(exec);

/**
 * Yield open-sourced Closure file paths and contents, one at a time.
 */
async function* yieldClosureFiles() {
  const {stdout} = await execP('git ls-files');
  const files = stdout.split('\n').filter(x => x);
  for (const filePath of files) {
    const ext = path.extname(filePath);
    if (IGNORED_EXTENSIONS.includes(ext) || IGNORED_FILES.includes(filePath) ||
        filePath.startsWith('third_party') || filePath.endsWith('/BUILD'))
      continue;
    yield {
      filePath,
      contents: (await fs.readFile(filePath, 'utf8')).split('\n')
    };
  }
}

/**
 * A collection of licensing and file reference checks for open-sourced Closure
 * files.
 */
class ClosureOSSChecker {
  /**
   * Checks whether a conformance textproto file contains only Closure paths.
   * @param {string} filePath The path to conformance_proto.txt.
   * @param {!Array<string>} contents The contents of conformance_proto.txt.
   * @return {!Array<string>} A list of error strings. An empty list indicates
   * that the check passes.
   */
  static hasOnlyClosurePathsInConformance(filePath, contents) {
    const errors = [];
    let lineNo = 1;
    for (const line of contents) {
      if (CONFORMANCE_ALLOWLIST_REGEX.test(line)) {
        errors.push(`${filePath}: Non-Closure path found on line ${lineNo}`);
      }
      lineNo++;
    }
    return errors;
  }

  /**
   * Checks whether the given file has a copyright notice for Closure Authors.
   * @param {string} filePath The path to the file to scan.
   * @param {!Array<string>} contents The contents of a file to scan.
   * @return {!Array<string>} A list of error strings. An empty list indicates
   * that the check passes.
   */
  static hasClosureCopyright(filePath, contents) {
    for (const line of contents) {
      if (CLOSURE_AUTHORS_COPYRIGHT_REGEX.test(line)) {
        return [];
      }
    }
    return [`${filePath}: Could not find Closure Authors copyright in file.`];
  }

  /**
   * Checks whether the given file mentions the Apache 2 license.
   * @param {string} filePath The path to the file to scan.
   * @param {!Array<string>} contents The contents of a file to scan.
   * @return {!Array<string>} A list of error strings. An empty list indicates
   * that the check passes.
   */
  static mentionsApache(filePath, contents) {
    for (const line of contents) {
      if (APACHE_LICENSE_REGEXES.some(regex => regex.test(line))) {
        return [];
      }
    }
    return [`${filePath}: File does not mention Apache License 2.0`];
  }

  /**
   * Checks whether the given file mentions the CC BY license.
   * @param {string} filePath The path to the file to scan.
   * @param {!Array<string>} contents The contents of a file to scan.
   * @return {!Array<string>} A list of error strings. An empty list indicates
   * that the check passes.
   */
  static mentionsCCBYLicense(filePath, contents) {
    let remaining = CC_BY_LICENSE_REGEXES;
    for (const line of contents) {
      remaining = remaining.filter(regex => !regex.test(line));
      if (remaining.length === 0) {
        return [];
      }
    }
    return [
      `${filePath}: Documentation file does not mention CC BY 4.0 license`
    ];
  }

  /**
   * Checks whether the given file is free of any Google copyright notices.
   * @param {string} filePath The path to the file to scan.
   * @param {!Array<string>} contents The contents of a file to scan.
   * @return {!Array<string>} A list of error strings. An empty list indicates
   * that the check passes.
   */
  static omitsGoogleCopyright(filePath, contents) {
    const errors = [];
    let lineNo = 1;
    for (const line of contents) {
      if (GOOGLE_COPYRIGHT_REGEX.test(line)) {
        errors.push(
            `${filePath}: Copyright Google statement found on line ${lineNo}`);
      }
      lineNo++;
    }
    return errors;
  }
}

/**
 * Checks that open-sourced files in this repository satisfy licensing and file
 * reference checks.
 * @return {!Promise<number>} The exit code.
 */
async function main() {
  const errors = [];

  const conformanceProto =
      (await fs.readFile(
           path.relative(`${__dirname}/..`, CONFORMANCE_PROTO_PATH), 'utf8'))
          .split('\n');
  errors.push(...ClosureOSSChecker.hasOnlyClosurePathsInConformance(
      CONFORMANCE_PROTO_PATH, conformanceProto));

  for await (const {filePath, contents} of yieldClosureFiles()) {
    // We check that there are no Google copyrights in every file except this
    // one, because the regex used to match for it (GOOGLE_COPYRIGHT_REGEX)
    // matches its own string representation.
    const isThisFile = filePath === path.relative(REPOSITORY_PATH, __filename);
    if (!isThisFile) {
      errors.push(
          ...ClosureOSSChecker.omitsGoogleCopyright(filePath, contents));
    }

    if (DOC_FILE_REGEX.test(filePath)) {
      errors.push(...ClosureOSSChecker.mentionsCCBYLicense(filePath, contents));
    } else if (!filePath.startsWith('doc/')) {
      errors.push(...ClosureOSSChecker.mentionsApache(filePath, contents));
      errors.push(...ClosureOSSChecker.hasClosureCopyright(filePath, contents));
    }
  }

  if (errors.length > 0) {
    console.error(errors.join('\n'));
    return 1;
  }
  console.error('All checks pass');
  return 0;
}

main().then(code => process.exit(code));