https://github.com/GeorgeKA updated https://github.com/llvm/llvm-project/pull/120900
>From faf8597dbb58a08991e11e9c4b9a0aad2f0b4234 Mon Sep 17 00:00:00 2001 From: GeorgeKA <k._asa...@hotmail.com> Date: Sun, 22 Dec 2024 09:42:36 -0500 Subject: [PATCH 1/3] [Clang][Driver][Test] Created test for unsupported driver options Created generate_unsupported_in_drivermode.py which generates a Lit regression test file that validates that options are only exposed to intended driver modes. The options and driver modes are parsed from Options.td, whose path should be provided on the command line. See clang/include/clang/Driver/Options.td The path to the TableGen executable can optionally be provided. Otherwise, the script will search for it. --- .../generate_unsupported_in_drivermode.py | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 clang/utils/generate_unsupported_in_drivermode.py diff --git a/clang/utils/generate_unsupported_in_drivermode.py b/clang/utils/generate_unsupported_in_drivermode.py new file mode 100644 index 00000000000000..f32c2c7c5a0d7c --- /dev/null +++ b/clang/utils/generate_unsupported_in_drivermode.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 + +""" generate_unsupported_in_drivermode.py + +usage: python generate_unsupported_in_drivermode.py <path>/Options.td [<path>/llvm-tblgen] + +This script generates a Lit regression test file that validates that options +are only exposed to intended driver modes. + +The options and driver modes are parsed from Options.td, whose path should be +provided on the command line. See clang/include/clang/Driver/Options.td + +The path to the TableGen executable can optionally be provided. Otherwise, the +script will search for it. + +Logic: +1) For each option, (records of class "Option"), and for each driver, (records of class "OptionVisibility") + a. if the option's "Visibility" field includes the driver flavour, skip processing this option for this driver + b. if the option is part of an option group, (the record has the "Group" property), + and the group's "Visibility" field includes the driver flavor, skip processing this option for this driver + c. otherwise this option is not supported by this driver flavor, and this pairing is saved for testing +2) For each unsupported pairing, generate a Lit RUN line, and a CHECK line to parse for expected output. Ex: "error: unknown argument" +""" + +import sys +import shutil +import os +import json +import subprocess +from pathlib import Path + +LLVM_TABLEGEN = "llvm-tblgen" +LIT_TEST_PATH = "../test/Driver/Inputs/unsupported-driver-options-check.ll" +INCLUDE_PATH = "../../llvm/include" +PREFIX = "CHECK-" + +# Strings used in Options.td for various driver flavours +OPTION_CC1AS = "CC1AsOption" +OPTION_CC1 = "CC1Option" +OPTION_CL = "CLOption" +OPTION_DXC = "DXCOption" +OPTION_DEFAULT = "DefaultVis" +OPTION_FC1 = "FC1Option" +OPTION_FLANG = "FlangOption" + +# Error messages output from each driver +ERROR_MSG_CC1AS = ": error: unknown argument" +ERROR_MSG_CC1 = "error: unknown argument" +ERROR_MSG_CL = "" # TODO +ERROR_MSG_DXC = "" # TODO +ERROR_MSG_DEFAULT = "clang: error: unknown argument" +ERROR_MSG_FC1 = "error: unknown argument" +ERROR_MSG_FLANG = "flang: error: unknown argument" + +# Lit CHECK prefixes +CHECK_PREFIX_CC1AS = PREFIX + OPTION_CC1AS +CHECK_PREFIX_CC1 = PREFIX + OPTION_CC1 +CHECK_PREFIX_CL = PREFIX + OPTION_CL +CHECK_PREFIX_DXC = PREFIX + OPTION_DXC +CHECK_PREFIX_DEFAULT = PREFIX + OPTION_DEFAULT +CHECK_PREFIX_FC1 = PREFIX + OPTION_FC1 +CHECK_PREFIX_FLANG = PREFIX + OPTION_FLANG + +LIT_TEST_NOTE = ("; NOTE: This lit test was automatically generated to validate " + + "unintentionally exposed arguments to various driver flavours.\n" + "; NOTE: To make changes, see " + Path(__file__).resolve().as_posix() + + " from which it was generated.\n\n") + +def print_usage(): + """ Print valid usage of this script + """ + sys.exit( "usage: python " + sys.argv[0] + " <path>/Options.td [<path>/llvm-tblgen]" ) + +def find_file(file_name, search_path): + """ Find the given file name under a search path + """ + result = [] + + for root, dir, files in os.walk(search_path): + if file_name in files: + result.append(os.path.join(root, file_name)) + return result + +def is_valid_file(path, expected_name): + """ Is a file valid + Check if a given path is to a file, and if it matches the expected file name + """ + if path.is_file() and path.name == expected_name: + return True + else: + return False + +def find_tablegen(): + """ Validate the TableGen executable + """ + result = shutil.which(LLVM_TABLEGEN) + if result is None: + sys.exit("Unable to find " + LLVM_TABLEGEN + ".\nExiting") + else: + print("TableGen found: " + result) + return result + +def find_groups(group_sequence, options_json, option): + """ Find the groups for a given option + Note that groups can themselves be part of groups, hence the recursion + """ + group_json = options_json[option]["Group"] + + if group_json is None: + return + + # Prevent circular group membership lookup + for group in group_sequence: + if group_json["def"] == group: + return + + group_sequence.append(group_json["def"]) + return find_groups(group_sequence, options_json, option) + + +class UnsupportedDriverOption(): + def __init__(self, driver, option): + self.driver = driver + self.option = option + +# Validate the number of arguments have been passed +argc = len(sys.argv) +if argc < 2 or argc > 3: + print_usage() + +options_input_path = Path(sys.argv[1]) +tablegen_input_path = "" +tablegen = None +options_td = "" +driver_sequence = [] +options_sequence = [] +unsupported_sequence = [] + +current_path = os.path.dirname(__file__) + +# Validate Options.td +if not is_valid_file(options_input_path, "Options.td"): + print("Invalid Options.td path. Searching for valid path...") + + relative_path = "../" + search_path = os.path.join(current_path, relative_path) + + file_search_list = find_file("Options.td", search_path) + if len(file_search_list) != 1: + print_usage() + sys.exit("Unable to find Options.td.\nExiting") + else: + options_td = file_search_list[0] + print(options_td) +else: + options_td = options_input_path.resolve().as_posix() + +# Validate TableGen executable +if argc > 2: + tablegen_input_path = Path(sys.argv[2]) + if not is_valid_file(tablegen_input_path, "llvm-tblgen"): + print("Invalid tablegen path. Searching for valid path...") + tablegen = find_tablegen() + else: + tablegen = tablegen_input_path.resolve().as_posix() +else: + tablegen = find_tablegen() + +# Run TableGen to convert Options.td to json +options_json_str = subprocess.run([ tablegen, "-I", os.path.join(current_path, INCLUDE_PATH), options_td, "-dump-json"], stdout=subprocess.PIPE) +options_json = json.loads(options_json_str.stdout.decode('utf-8')) + +# Gather list of driver flavours +for i in options_json["!instanceof"]["OptionVisibility"]: + driver_sequence.append(i) + +# Gather list of options +for i in options_json["!instanceof"]["Option"]: + options_sequence.append(i) + +# Walk through the options list and find which drivers shouldn't be visible to each option +for option in options_sequence: + tmp_vis_list = [] + group_sequence = [] + + # Check for the option's explicit visibility + for visibility in options_json[option]["Visibility"]: + tmp_vis_list.append(visibility["def"]) + + # Check for the option's group's visibility + find_groups(group_sequence, options_json, option) + if len(group_sequence) > 0: + for group_name in group_sequence: + for visibility in options_json[group_name]["Visibility"]: + tmp_vis_list.append(visibility["def"]) + + # Append to the unsupported list + for driver in driver_sequence: + if driver not in tmp_vis_list: + unsupported_sequence.append(UnsupportedDriverOption(driver, option)) + +# Generate the lit test +try: + with open(LIT_TEST_PATH, "w") as lit_file: + try: + lit_file.write(LIT_TEST_NOTE) + + for i in unsupported_sequence: + if i.driver == OPTION_CC1AS: + lit_file.write( + "; RUN: not clang -cc1as -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_CC1AS + " %s\n") + continue + if i.driver == OPTION_CC1: + lit_file.write( + "; RUN: not clang -cc1 -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_CC1 + " %s\n") + continue + # if i.driver == OPTION_CL: + # lit_file.write( + # "; RUN: not clang-cl -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_CL + " %s\n") + # continue + # if i.driver == OPTION_DXC: + # lit_file.write( + # "; RUN: not clang-dxc -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_DXC + " %s\n") + # continue + if i.driver == OPTION_DEFAULT: + lit_file.write( + "; RUN: not clang -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_DEFAULT + " %s\n") + continue + if i.driver == OPTION_FC1: + lit_file.write( + "; RUN: not flang -fc1 -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_FC1 + " %s\n") + continue + if i.driver == OPTION_FLANG: + lit_file.write( + "; RUN: not flang -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_FLANG + " %s\n") + + lit_file.write("; " + CHECK_PREFIX_CC1AS + ": " + ERROR_MSG_CC1AS + "\n") + lit_file.write("; " + CHECK_PREFIX_CC1 + ": " + ERROR_MSG_CC1 + "\n") + lit_file.write("; " + CHECK_PREFIX_CL + ": " + ERROR_MSG_CL + "\n") + lit_file.write("; " + CHECK_PREFIX_DXC + ": " + ERROR_MSG_DXC + "\n") + lit_file.write("; " + CHECK_PREFIX_DEFAULT + ": " + ERROR_MSG_DEFAULT + "\n") + lit_file.write("; " + CHECK_PREFIX_FC1 + ": " + ERROR_MSG_FC1 + "\n") + lit_file.write("; " + CHECK_PREFIX_FLANG + ": " + ERROR_MSG_FLANG + "\n") + except(IOError, OSError): + sys.exit("Error writing to " + "LIT_TEST_PATH. Exiting") +except(FileNotFoundError, PermissionError, OSError): + sys.exit("Error opening " + "LIT_TEST_PATH" + ". Exiting") +else: + lit_file.close() \ No newline at end of file >From 3a4cbef26a269001bf09f0565827a8ff746a851f Mon Sep 17 00:00:00 2001 From: GeorgeKA <k._asa...@hotmail.com> Date: Mon, 23 Dec 2024 16:51:42 -0500 Subject: [PATCH 2/3] Fixed formatting and changed to common error msg check Changed to a common error message for the various driver modes, and formatted with Python darker. --- .../generate_unsupported_in_drivermode.py | 126 ++++++++++-------- 1 file changed, 67 insertions(+), 59 deletions(-) diff --git a/clang/utils/generate_unsupported_in_drivermode.py b/clang/utils/generate_unsupported_in_drivermode.py index f32c2c7c5a0d7c..b4fab71154edef 100644 --- a/clang/utils/generate_unsupported_in_drivermode.py +++ b/clang/utils/generate_unsupported_in_drivermode.py @@ -32,7 +32,6 @@ LLVM_TABLEGEN = "llvm-tblgen" LIT_TEST_PATH = "../test/Driver/Inputs/unsupported-driver-options-check.ll" INCLUDE_PATH = "../../llvm/include" -PREFIX = "CHECK-" # Strings used in Options.td for various driver flavours OPTION_CC1AS = "CC1AsOption" @@ -43,37 +42,27 @@ OPTION_FC1 = "FC1Option" OPTION_FLANG = "FlangOption" -# Error messages output from each driver -ERROR_MSG_CC1AS = ": error: unknown argument" -ERROR_MSG_CC1 = "error: unknown argument" -ERROR_MSG_CL = "" # TODO -ERROR_MSG_DXC = "" # TODO -ERROR_MSG_DEFAULT = "clang: error: unknown argument" -ERROR_MSG_FC1 = "error: unknown argument" -ERROR_MSG_FLANG = "flang: error: unknown argument" - -# Lit CHECK prefixes -CHECK_PREFIX_CC1AS = PREFIX + OPTION_CC1AS -CHECK_PREFIX_CC1 = PREFIX + OPTION_CC1 -CHECK_PREFIX_CL = PREFIX + OPTION_CL -CHECK_PREFIX_DXC = PREFIX + OPTION_DXC -CHECK_PREFIX_DEFAULT = PREFIX + OPTION_DEFAULT -CHECK_PREFIX_FC1 = PREFIX + OPTION_FC1 -CHECK_PREFIX_FLANG = PREFIX + OPTION_FLANG - -LIT_TEST_NOTE = ("; NOTE: This lit test was automatically generated to validate " + - "unintentionally exposed arguments to various driver flavours.\n" - "; NOTE: To make changes, see " + Path(__file__).resolve().as_posix() - + " from which it was generated.\n\n") +# See clang/include/clang/Basic/DiagnosticDriverKinds.td for the *unknown_argument* strings +# As per Driver::ParseArgStrings from Driver.cpp, all the driver modes use the +# string "unknown argument" in their unsupported option error messages +ERROR_MSG_GENERAL = "unknown argument" + +RUN_CMD_END = " -help 2>&1 | FileCheck %s\n" + +LIT_TEST_NOTE = ( + "; NOTE: This lit test was automatically generated to validate " + + "unintentionally exposed arguments to various driver flavours.\n" + "; NOTE: To make changes, see " + + Path(__file__).resolve().as_posix() + + " from which it was generated.\n\n" +) def print_usage(): - """ Print valid usage of this script - """ - sys.exit( "usage: python " + sys.argv[0] + " <path>/Options.td [<path>/llvm-tblgen]" ) + """Print valid usage of this script""" + sys.exit("usage: python " + sys.argv[0] + " <path>/Options.td [<path>/llvm-tblgen]") def find_file(file_name, search_path): - """ Find the given file name under a search path - """ + """Find the given file name under a search path""" result = [] for root, dir, files in os.walk(search_path): @@ -82,7 +71,7 @@ def find_file(file_name, search_path): return result def is_valid_file(path, expected_name): - """ Is a file valid + """Is a file valid Check if a given path is to a file, and if it matches the expected file name """ if path.is_file() and path.name == expected_name: @@ -91,8 +80,7 @@ def is_valid_file(path, expected_name): return False def find_tablegen(): - """ Validate the TableGen executable - """ + """Validate the TableGen executable""" result = shutil.which(LLVM_TABLEGEN) if result is None: sys.exit("Unable to find " + LLVM_TABLEGEN + ".\nExiting") @@ -101,7 +89,7 @@ def find_tablegen(): return result def find_groups(group_sequence, options_json, option): - """ Find the groups for a given option + """Find the groups for a given option Note that groups can themselves be part of groups, hence the recursion """ group_json = options_json[option]["Group"] @@ -118,7 +106,7 @@ def find_groups(group_sequence, options_json, option): return find_groups(group_sequence, options_json, option) -class UnsupportedDriverOption(): +class UnsupportedDriverOption: def __init__(self, driver, option): self.driver = driver self.option = option @@ -167,8 +155,17 @@ def __init__(self, driver, option): tablegen = find_tablegen() # Run TableGen to convert Options.td to json -options_json_str = subprocess.run([ tablegen, "-I", os.path.join(current_path, INCLUDE_PATH), options_td, "-dump-json"], stdout=subprocess.PIPE) -options_json = json.loads(options_json_str.stdout.decode('utf-8')) +options_json_str = subprocess.run( + [ + tablegen, + "-I", + os.path.join(current_path, INCLUDE_PATH), + options_td, + "-dump-json", + ], + stdout=subprocess.PIPE, +) +options_json = json.loads(options_json_str.stdout.decode("utf-8")) # Gather list of driver flavours for i in options_json["!instanceof"]["OptionVisibility"]: @@ -185,7 +182,8 @@ def __init__(self, driver, option): # Check for the option's explicit visibility for visibility in options_json[option]["Visibility"]: - tmp_vis_list.append(visibility["def"]) + if visibility is not None: + tmp_vis_list.append(visibility["def"]) # Check for the option's group's visibility find_groups(group_sequence, options_json, option) @@ -208,42 +206,52 @@ def __init__(self, driver, option): for i in unsupported_sequence: if i.driver == OPTION_CC1AS: lit_file.write( - "; RUN: not clang -cc1as -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_CC1AS + " %s\n") + "; RUN: not clang -cc1as -" + + i.option + + RUN_CMD_END) continue if i.driver == OPTION_CC1: lit_file.write( - "; RUN: not clang -cc1 -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_CC1 + " %s\n") + "; RUN: not clang -cc1 -" + + i.option + + RUN_CMD_END) + continue + if i.driver == OPTION_CL: + lit_file.write( + "; RUN: not clang-cl -" + + i.option + + RUN_CMD_END) + continue + if i.driver == OPTION_DXC: + lit_file.write( + "; RUN: not clang-dxc -" + + i.option + + RUN_CMD_END) continue - # if i.driver == OPTION_CL: - # lit_file.write( - # "; RUN: not clang-cl -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_CL + " %s\n") - # continue - # if i.driver == OPTION_DXC: - # lit_file.write( - # "; RUN: not clang-dxc -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_DXC + " %s\n") - # continue if i.driver == OPTION_DEFAULT: lit_file.write( - "; RUN: not clang -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_DEFAULT + " %s\n") + "; RUN: not clang -" + + i.option + + RUN_CMD_END) continue if i.driver == OPTION_FC1: lit_file.write( - "; RUN: not flang -fc1 -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_FC1 + " %s\n") + "; RUN: not flang -fc1 -" + + i.option + + RUN_CMD_END) continue if i.driver == OPTION_FLANG: lit_file.write( - "; RUN: not flang -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_FLANG + " %s\n") - - lit_file.write("; " + CHECK_PREFIX_CC1AS + ": " + ERROR_MSG_CC1AS + "\n") - lit_file.write("; " + CHECK_PREFIX_CC1 + ": " + ERROR_MSG_CC1 + "\n") - lit_file.write("; " + CHECK_PREFIX_CL + ": " + ERROR_MSG_CL + "\n") - lit_file.write("; " + CHECK_PREFIX_DXC + ": " + ERROR_MSG_DXC + "\n") - lit_file.write("; " + CHECK_PREFIX_DEFAULT + ": " + ERROR_MSG_DEFAULT + "\n") - lit_file.write("; " + CHECK_PREFIX_FC1 + ": " + ERROR_MSG_FC1 + "\n") - lit_file.write("; " + CHECK_PREFIX_FLANG + ": " + ERROR_MSG_FLANG + "\n") + "; RUN: not flang -" + + i.option + + RUN_CMD_END) + + lit_file.write( + "; CHECK: "+ ERROR_MSG_GENERAL + "\n" + ) except(IOError, OSError): sys.exit("Error writing to " + "LIT_TEST_PATH. Exiting") -except(FileNotFoundError, PermissionError, OSError): +except (FileNotFoundError, PermissionError, OSError): sys.exit("Error opening " + "LIT_TEST_PATH" + ". Exiting") else: - lit_file.close() \ No newline at end of file + lit_file.close() >From 454a314bfb844a9148b37af7051b6366af5d67dc Mon Sep 17 00:00:00 2001 From: GeorgeKA <k._asa...@hotmail.com> Date: Sun, 5 Jan 2025 18:57:38 -0500 Subject: [PATCH 3/3] A variety of additions to more properly generate the Lit test. - Added handling for false positives caused by supported options that are prefixes for unsupported ones. - Added a controller to simplify modifying the test commands for each driver. - Generally cleaned things up - A list of exceptions called exceptions_sequence that needs to be fixed - clang-cl & clang-dxc testing remains --- .../generate_unsupported_in_drivermode.py | 348 +++++++++++++----- 1 file changed, 248 insertions(+), 100 deletions(-) diff --git a/clang/utils/generate_unsupported_in_drivermode.py b/clang/utils/generate_unsupported_in_drivermode.py index b4fab71154edef..bdb0b5d6d33807 100644 --- a/clang/utils/generate_unsupported_in_drivermode.py +++ b/clang/utils/generate_unsupported_in_drivermode.py @@ -27,42 +27,98 @@ import os import json import subprocess +import math from pathlib import Path LLVM_TABLEGEN = "llvm-tblgen" LIT_TEST_PATH = "../test/Driver/Inputs/unsupported-driver-options-check.ll" INCLUDE_PATH = "../../llvm/include" -# Strings used in Options.td for various driver flavours -OPTION_CC1AS = "CC1AsOption" -OPTION_CC1 = "CC1Option" -OPTION_CL = "CLOption" -OPTION_DXC = "DXCOption" -OPTION_DEFAULT = "DefaultVis" -OPTION_FC1 = "FC1Option" -OPTION_FLANG = "FlangOption" +# Strings defined in Options.td for the various driver flavours. See "OptionVisibility" +VISIBILITY_CC1AS = "CC1AsOption" +VISIBILITY_CC1 = "CC1Option" +VISIBILITY_CL = "CLOption" +VISIBILITY_DXC = "DXCOption" +VISIBILITY_DEFAULT = "DefaultVis" +VISIBILITY_FC1 = "FC1Option" +VISIBILITY_FLANG = "FlangOption" + +# Strings used in the commands to be tested +CLANG = "clang" +CLANG_CL = "clang-cl" +CLANG_DXC = "clang-dxc" +FLANG = "flang-new" +OPTION_NUM = "-###" +OPTION_X = "-x" +OPTION_CPP = "c++" +OPTION_C = "-c" +LIT_CMD_END = " - < /dev/null 2>&1 | FileCheck %s\n" # See clang/include/clang/Basic/DiagnosticDriverKinds.td for the *unknown_argument* strings # As per Driver::ParseArgStrings from Driver.cpp, all the driver modes use the # string "unknown argument" in their unsupported option error messages -ERROR_MSG_GENERAL = "unknown argument" +ERROR_MSG_CHECK = ("{{(unknown argument|" + "argument unused|" + "unsupported|" + "unknown integrated tool)}}") + +LIT_TEST_NOTE = ("; NOTE: This lit test was automatically generated to validate " + "unintentionally exposed arguments to various driver flavours.\n" + "; NOTE: To make changes, see " + Path(__file__).resolve().as_posix() + + " from which it was generated.\n" + "To output which unsupported options are not tested by this Lit" + " test, see that script\n\n") + +exceptions_sequence = ["Wno_rewrite_macros", # Default + "fexperimental_sanitize_metadata_EQ_atomics", # Default + "fexperimental_sanitize_metadata_EQ_covered", # Default + "fexperimental_sanitize_metadata_EQ_uar", # Default + "mno_strict_align", # CC1 + "mstrict_align", + "fheinous-gnu-extensions", + "fcuda-approx-transcendentals"] # CC1 TODO: This is temporary + +class DriverController: + """ Controller for data specific to each driver + shell_cmd_prefix: The beginning string of the command to be tested + visibility_str: The corresponding visibility string from OptionVisibility in Options.td + shell_cmd_suffix: Strings near the end of the command to be tested + supported_sequence: List of UnsupportedDriverOption objects for supported options + that are Kind KIND_JOINED*, as defined in Options.td + is_os_compatible: Boolean indicating whether this driver is available on the current OS + """ + def __init__(self, shell_cmd_prefix = "", visibility_str = "", shell_cmd_suffix = "", is_os_compatible = False): + self.shell_cmd_prefix = shell_cmd_prefix + self.visibility_str = visibility_str + self.shell_cmd_suffix = shell_cmd_suffix + self.supported_sequence = [] + self.is_os_compatible = is_os_compatible -RUN_CMD_END = " -help 2>&1 | FileCheck %s\n" +class UnsupportedDriverOption: + """ Defines an unsupported driver-option combination + driver: The driver string as defined by OptionVisibility in Options.td + option: The option string. See "Name" for a given option in Options.td + prefix: String that precedes the option. Ex. "-" + is_error: Boolean indicating whether the corresponding command generates an error + """ + def __init__(self, driver, option, prefix): + self.driver = driver + self.option = option + self.prefix = prefix + self.is_error = True -LIT_TEST_NOTE = ( - "; NOTE: This lit test was automatically generated to validate " - + "unintentionally exposed arguments to various driver flavours.\n" - "; NOTE: To make changes, see " - + Path(__file__).resolve().as_posix() - + " from which it was generated.\n\n" -) + # For sorting + def __len__(self): + return len(self.option) def print_usage(): - """Print valid usage of this script""" - sys.exit("usage: python " + sys.argv[0] + " <path>/Options.td [<path>/llvm-tblgen]") + """ Print valid usage of this script + """ + sys.exit( "usage: python " + sys.argv[0] + " <path>/Options.td [<path>/llvm-tblgen]" ) def find_file(file_name, search_path): - """Find the given file name under a search path""" + """ Find the given file name under a search path + """ result = [] for root, dir, files in os.walk(search_path): @@ -71,7 +127,7 @@ def find_file(file_name, search_path): return result def is_valid_file(path, expected_name): - """Is a file valid + """ Is a file valid Check if a given path is to a file, and if it matches the expected file name """ if path.is_file() and path.name == expected_name: @@ -79,17 +135,28 @@ def is_valid_file(path, expected_name): else: return False +def find_executable(executable): + """ Validate an executable + """ + result = shutil.which(executable) + if result is None: + print(f"Unable to find {executable}") + else: + print(f"{executable} found: {result}") + + return result + def find_tablegen(): - """Validate the TableGen executable""" - result = shutil.which(LLVM_TABLEGEN) + """ Validate the TableGen executable + """ + result = find_executable(LLVM_TABLEGEN) if result is None: - sys.exit("Unable to find " + LLVM_TABLEGEN + ".\nExiting") + sys.exit("\nExiting") else: - print("TableGen found: " + result) return result def find_groups(group_sequence, options_json, option): - """Find the groups for a given option + """ Find the groups for a given option Note that groups can themselves be part of groups, hence the recursion """ group_json = options_json[option]["Group"] @@ -105,12 +172,6 @@ def find_groups(group_sequence, options_json, option): group_sequence.append(group_json["def"]) return find_groups(group_sequence, options_json, option) - -class UnsupportedDriverOption: - def __init__(self, driver, option): - self.driver = driver - self.option = option - # Validate the number of arguments have been passed argc = len(sys.argv) if argc < 2 or argc > 3: @@ -123,6 +184,12 @@ def __init__(self, driver, option): driver_sequence = [] options_sequence = [] unsupported_sequence = [] +# List of driver-option pairs that will be skipped due to +# overlapping supported and unsupported option names. See later comments for detail +skipped_sequence = [] +# List of driver-option pairs that will be skipped due to +# a variety of limitations. See usage for detail +untested_sequence = [] current_path = os.path.dirname(__file__) @@ -155,30 +222,59 @@ def __init__(self, driver, option): tablegen = find_tablegen() # Run TableGen to convert Options.td to json -options_json_str = subprocess.run( - [ - tablegen, - "-I", - os.path.join(current_path, INCLUDE_PATH), - options_td, - "-dump-json", - ], - stdout=subprocess.PIPE, -) -options_json = json.loads(options_json_str.stdout.decode("utf-8")) +options_json_str = subprocess.run([ tablegen, "-I", os.path.join(current_path, INCLUDE_PATH), options_td, "-dump-json"], stdout=subprocess.PIPE) +options_json = json.loads(options_json_str.stdout.decode('utf-8')) + +# Establish the controller objects for each driver +driver_cc1as = DriverController(f"{CLANG} -cc1as", VISIBILITY_CC1AS, "", None != find_executable(CLANG)) +driver_cc1 = DriverController(f"{CLANG} -cc1", VISIBILITY_CC1, " " + OPTION_X + " " + OPTION_CPP, None != find_executable(CLANG)) +driver_cl = DriverController(CLANG_CL, VISIBILITY_CL, " " + OPTION_NUM + " " + OPTION_X + " " + OPTION_CPP + " " + OPTION_C, None != find_executable(CLANG_CL)) +driver_dxc = DriverController(CLANG_DXC, VISIBILITY_DXC, " " + OPTION_NUM + " " + OPTION_X + " " + OPTION_CPP + " " + OPTION_C, None != find_executable(CLANG_DXC)) +driver_default = DriverController(CLANG, VISIBILITY_DEFAULT, " " + OPTION_NUM + " " + OPTION_X + " " + OPTION_CPP + " " + OPTION_C, None != find_executable(CLANG)) +driver_fc1 = DriverController(f"{FLANG} -fc1", VISIBILITY_FC1, "", None != find_executable(FLANG)) +driver_flang = DriverController(FLANG, VISIBILITY_FLANG, " " + OPTION_NUM + " " + OPTION_X + " " + OPTION_CPP + " " + OPTION_C, None != find_executable(FLANG)) + +driver_controller = [driver_cc1as, driver_cc1, driver_cl, driver_dxc, driver_default, driver_fc1, driver_flang] + +def get_index(driver_vis): + """ Get the driver controller index for a given driver + driver_vis: The visibility string from OptionVisibility in Options.td + """ + for index, driver_ctrl in enumerate(driver_controller): + if driver_vis == driver_ctrl.visibility_str: + return index # Gather list of driver flavours -for i in options_json["!instanceof"]["OptionVisibility"]: - driver_sequence.append(i) - -# Gather list of options -for i in options_json["!instanceof"]["Option"]: - options_sequence.append(i) +for visibility in options_json["!instanceof"]["OptionVisibility"]: + driver_sequence.append(visibility) # Walk through the options list and find which drivers shouldn't be visible to each option -for option in options_sequence: +for option in options_json["!instanceof"]["Option"]: + kind = options_json[option]["Kind"]["def"] + should_skip = False tmp_vis_list = [] group_sequence = [] + option_name = options_json[option]["Name"] + + # There are a few conditions that make an option unsuitable to test in this script + # Options of kind KIND_INPUT & KIND_UNKNOWN don't apply to this test. For example, + # Option "INPUT" with name "<input>". + if option in exceptions_sequence or \ + options_json[option]["Name"] is None or \ + kind == "KIND_INPUT" or \ + kind == "KIND_UNKNOWN": + + untested_sequence.append(UnsupportedDriverOption("All", option, "")) + continue + + # Get the correct option prefix + prefixes = options_json[option]["Prefixes"] + prefix = "" + if prefixes is not None and len(prefixes) > 0: + # Assuming the first prefix is the preferred prefix + prefix = prefixes[0] + if os.name != "nt" and prefix == "/": + continue # Check for the option's explicit visibility for visibility in options_json[option]["Visibility"]: @@ -189,69 +285,121 @@ def __init__(self, driver, option): find_groups(group_sequence, options_json, option) if len(group_sequence) > 0: for group_name in group_sequence: + # For clang_ignored_f_Group & f_Group see description in Options.td + # "Temporary groups for clang options which we know we don't support, + # but don't want to verbosely warn the user about." + if group_name == "clang_ignored_f_Group" or group_name == "f_Group": + should_skip = True + break for visibility in options_json[group_name]["Visibility"]: tmp_vis_list.append(visibility["def"]) + if should_skip: + untested_sequence.append(UnsupportedDriverOption("All", option, "")) + continue - # Append to the unsupported list + # KIND_JOINED* options that are supported need to be saved for checking + # which options cannot be validated with this script + is_option_kind_joined = kind == "KIND_JOINED" or kind == "KIND_JOINED_OR_SEPARATE" + + # Append to the unsupported list, and the various supported lists for driver in driver_sequence: if driver not in tmp_vis_list: - unsupported_sequence.append(UnsupportedDriverOption(driver, option)) - -# Generate the lit test + unsupported_sequence.append(UnsupportedDriverOption(driver, option_name, prefix)) + elif is_option_kind_joined: + driver_controller[get_index(driver)].supported_sequence.append(UnsupportedDriverOption(driver, option_name, prefix)) + +def find_supported_seq_cmp_start(supported_sequence, low, high, search_option): + """ Return the index where to start comparisons in the supported sequence + Modified binary search for the first element of supported_sequence + that has an option that is of equal or lesser length than the search option + from the unsupported sequence + The supported sequence must be reverse sorted by option name length + """ + middle = math.floor(low + (high - low) / 2) + + if low > high: + return -1 + # If the start of the list is reached + if middle - 1 == -1: + return middle + # If the end of the list is reached + if middle == len(supported_sequence)-1: + return middle + + if len(supported_sequence[middle].option) <= len(search_option) < len(supported_sequence[middle - 1].option): + return middle + elif len(supported_sequence[middle].option) <= len(search_option): + return find_supported_seq_cmp_start(supported_sequence, low, middle - 1, search_option) + elif len(supported_sequence[middle].option) > len(search_option): + return find_supported_seq_cmp_start(supported_sequence, middle+1, high, search_option) + else: + # No-op + return -1 + +# Sort the supported lists for the next block +for driver_ctrl in driver_controller: + driver_ctrl.supported_sequence.sort(key=len, reverse=True) + +# For a given driver, this script cannot generate tests for unsupported options +# that have a prefix that is a supported option of Kind KIND_JOINED*. +# These driver-option pairs are removed here. +for unsupported_pair in unsupported_sequence: + supported_seq = driver_controller[get_index(unsupported_pair.driver)].supported_sequence + start_index = find_supported_seq_cmp_start(supported_seq, 0, len(supported_seq)-1, unsupported_pair.option) + start_index = 0 if start_index == -1 else start_index + + for supported_pair in driver_controller[get_index(unsupported_pair.driver)].supported_sequence[start_index:]: + if unsupported_pair.option.startswith(supported_pair.option): + skipped_sequence.append(unsupported_pair) + +for skip_pair in skipped_sequence: + unsupported_sequence.remove(skip_pair) + +# Preprocess each default driver command to determine if they result in an error status or a warning +# This is necessary since the Lit tests require an explicit "; RUN: not" for errors +for unsupported_pair in unsupported_sequence: + if (driver_controller[get_index(unsupported_pair.driver)].is_os_compatible and + driver_controller[get_index(unsupported_pair.driver)].visibility_str == VISIBILITY_DEFAULT): + # Run each command inside the script + cmd = [f"{driver_controller[get_index(unsupported_pair.driver)].shell_cmd_prefix} \ + {unsupported_pair.prefix}{unsupported_pair.option} \ + {driver_controller[get_index(unsupported_pair.driver)].shell_cmd_suffix} -"] + cmd_out = subprocess.run( cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True) + unsupported_pair.is_error = True if cmd_out.returncode == 1 else False + +# Generate the Lit test try: with open(LIT_TEST_PATH, "w") as lit_file: try: lit_file.write(LIT_TEST_NOTE) - for i in unsupported_sequence: - if i.driver == OPTION_CC1AS: - lit_file.write( - "; RUN: not clang -cc1as -" - + i.option - + RUN_CMD_END) - continue - if i.driver == OPTION_CC1: - lit_file.write( - "; RUN: not clang -cc1 -" - + i.option - + RUN_CMD_END) - continue - if i.driver == OPTION_CL: - lit_file.write( - "; RUN: not clang-cl -" - + i.option - + RUN_CMD_END) - continue - if i.driver == OPTION_DXC: - lit_file.write( - "; RUN: not clang-dxc -" - + i.option - + RUN_CMD_END) - continue - if i.driver == OPTION_DEFAULT: - lit_file.write( - "; RUN: not clang -" - + i.option - + RUN_CMD_END) - continue - if i.driver == OPTION_FC1: - lit_file.write( - "; RUN: not flang -fc1 -" - + i.option - + RUN_CMD_END) - continue - if i.driver == OPTION_FLANG: - lit_file.write( - "; RUN: not flang -" - + i.option - + RUN_CMD_END) + for unsupported_pair in unsupported_sequence: + if unsupported_pair.is_error: + lit_not = "not " + else: + lit_not = "" + + CMD_START = "; RUN: " + lit_not - lit_file.write( - "; CHECK: "+ ERROR_MSG_GENERAL + "\n" - ) + if driver_controller[get_index(unsupported_pair.driver)].is_os_compatible: + lit_file.write( + CMD_START + + driver_controller[get_index(unsupported_pair.driver)].shell_cmd_prefix + + " " + + unsupported_pair.prefix + + unsupported_pair.option + + driver_controller[get_index(unsupported_pair.driver)].shell_cmd_suffix + + LIT_CMD_END) + lit_file.write("; CHECK: " + ERROR_MSG_CHECK + "\n") except(IOError, OSError): sys.exit("Error writing to " + "LIT_TEST_PATH. Exiting") -except (FileNotFoundError, PermissionError, OSError): +except(FileNotFoundError, PermissionError, OSError): sys.exit("Error opening " + "LIT_TEST_PATH" + ". Exiting") else: lit_file.close() + +# print("\nThese unsupported driver-option pairs were not tested:") +# for untested_pair in untested_sequence: +# print(f"Driver: {untested_pair.driver}\tOption:{untested_pair.option}") +# for skipped_pair in skipped_sequence: +# print(f"Driver: {skipped_pair.driver}\tOption:{skipped_pair.option}") _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits