================
@@ -0,0 +1,626 @@
+#!/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.
+
+The primary maintenance task for this script would be updating the expected 
return message for a driver mode if
+there are changes over time. See the instantiations of DriverController, 
specifically the check_string.
+
+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 flavour, skip 
processing this option for this driver
+    c. otherwise this option is not supported by this driver flavour, 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
+import math
+from pathlib import Path
+
+LLVM_TABLEGEN = "llvm-tblgen"
+LIT_TEST_PATH = "../test/Driver/unsupported_in_drivermode.c"
+LIT_TEST_PATH_FLANG = "../test/Driver/flang/unsupported_in_flang.f90"
+INCLUDE_PATH = "../../llvm/include"
+
+# 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 = f"{CLANG} --driver-mode=cl"
+CLANG_DXC = f"{CLANG} --driver-mode=dxc"
+FLANG = f"{CLANG} --driver-mode=flang"
+CLANG_LIT = "%clang"
+CLANG_CL_LIT = "%clang_cl"
+CLANG_DXC_LIT = "%clang_dxc"
+FLANG_LIT = f"%{FLANG}"
+OPTION_HASH = "-###"
+OPTION_X = "-x"
+OPTION_WX = "/WX"
+OPTION_CPP = "c++"
+OPTION_C = "-c"
+OPTION_CC1 = "-cc1"
+OPTION_CC1AS = "-cc1as"
+OPTION_FC1 = "-fc1"
+OPTION_SLASH_C = "/c"
+OPTION_T = "/T lib_6_7"
+SLASH_SLASH = "// "
+EXCLAMATION = "! "
+
+# Invalid usage of the driver options below causes unique output, so skip 
testing
+exceptions_sequence = [
+    "cc1",
+    "cc1as",
+]
+
+
+class DriverController:
+    """Controller for data specific to each driver
+    shell_cmd_prefix: The beginning string of the command to be tested
+    lit_cmd_prefix: The beginning string of the Lit command
+    visibility_str: The corresponding visibility string from OptionVisibility 
in Options.td
+    shell_cmd_suffix: Strings near the end of the command to be tested
+    check_string: The string or regex to be sent to FileCheck
+    lit_cmd_end: String at the end of the Lit command
+
+    supported_sequence: List of UnsupportedDriverOption objects for supported 
options
+                        that are Kind KIND_JOINED*, as defined in Options.td
+    """
+
+    def __init__(
+        self,
+        shell_cmd_prefix="",
+        lit_cmd_prefix="",
+        visibility_str="",
+        shell_cmd_suffix="",
+        check_string="{{(unknown argument|n?N?o such file or directory)}}",
+        lit_cmd_end=" - < /dev/null 2>&1 | FileCheck -check-prefix=",
+    ):
+        self.shell_cmd_prefix = shell_cmd_prefix
+        self.lit_cmd_prefix = lit_cmd_prefix
+        self.visibility_str = visibility_str
+        self.shell_cmd_suffix = shell_cmd_suffix
+        self.supported_sequence = []
+        self.check_string = check_string
+        self.lit_cmd_end = lit_cmd_end
+
+
+class UnsupportedDriverOption:
+    """Defines an unsupported driver-option combination
+    driver: The driver string as defined by OptionVisibility in Options.td
+    option: The option object from Options.td
+    option_name: Corresponding string for an option. 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, option_name, prefix):
+        self.driver = driver
+        self.option = option
+        self.option_name = option_name
+        self.prefix = prefix
+        self.is_error = True
+
+    # For sorting
+    def __len__(self):
+        return len(self.option_name)
+
+
+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:
+        print(f"Unable to find {LLVM_TABLEGEN}")
+        sys.exit("\nExiting")
+    else:
+        print(f"{LLVM_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_sequence: A sequence to which group names will be appended.
+    options_json: The converted Python dictionary from the Options.td json 
string
+    option: The option object from Options.td
+    """
+    group_json = options_json[option]["Group"]
+
+    if group_json is None:
+        return
+
+    # Prevent circular group membership lookup
+    if len(group_sequence) > 0:
+        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)
+
+
+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
+
+
+def get_visibility(option, filtered_visibility):
+    """Get a list of drivers that a given option is exposed to
+    option: The option object from Options.td
+    filtered_visibility: Sequence in which the visibility will be stored
+    """
+    group_sequence = []
+
+    # Check for the option's explicit visibility
+    for visibility in options_json[option]["Visibility"]:
+        if visibility is not None:
+            filtered_visibility.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"]:
+                filtered_visibility.append(visibility["def"])
+
+
+def find_supported_seq_cmp_start(supported_sequence, low, high, search_option):
+    """Return the index corresponding to 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_name)
+        <= len(search_option)
+        < len(supported_sequence[middle - 1].option_name)
+    ):
+        return middle
+    elif len(supported_sequence[middle].option_name) <= len(search_option):
+        return find_supported_seq_cmp_start(
+            supported_sequence, low, middle - 1, search_option
+        )
+    elif len(supported_sequence[middle].option_name) > len(search_option):
+        return find_supported_seq_cmp_start(
+            supported_sequence, middle + 1, high, search_option
+        )
+    else:
+        # No-op
+        return -1
+
+
+def get_lit_test_note(test_visibility):
+    """Return the note to be included at the start of the Lit test file
+    test_visibility: Any VISIBILITY_* variable. VISIBILITY_FLANG will return 
the .f90 formatted test note.
+    All other will return the .c formatted test note
+    """
+    test_prefix = EXCLAMATION if test_visibility == VISIBILITY_FLANG else 
SLASH_SLASH
+
+    return (
+        f"{test_prefix}UNSUPPORTED: system-windows\n"
+        f"{test_prefix}NOTE: This lit test was automatically generated to 
validate "
+        "unintentionally exposed arguments to various driver flavours.\n"
+        f"{test_prefix}NOTE: To make changes, see "
+        + Path(__file__).resolve().as_posix()
+        + " from which it was generated.\n\n"
+    )
+
+
+def write_lit_test(test_path, test_visibility, unsupported_list):
+    """Write the Lit tests to file
+    test_path: File write path
+    test_visibility: VISIBILITY_DEFAULT or VISIBILITY_FLANG, which indicates 
whether to write
+    to the main Lit test file or flang Lit test file respectively
+    unsupported_list: List of UnsupportedDriverOption objects
+    """
+    try:
+        with open(test_path, "w") as lit_file:
+            try:
+                lit_file.write(get_lit_test_note(test_visibility))
+
+                for index, unsupported_pair in enumerate(unsupported_list):
+                    is_flang_pair = (
+                        unsupported_pair.driver == VISIBILITY_FLANG
+                        or unsupported_pair.driver == VISIBILITY_FC1
+                    )
+                    if (test_visibility == VISIBILITY_FLANG and not 
is_flang_pair) or (
+                        test_visibility == VISIBILITY_DEFAULT and is_flang_pair
+                    ):
+                        continue
+
+                    # In testing, return codes cannot be relied on 
consistently for assessing command failure.
+                    # Leaving this handling here in case things change, but 
for now, Lit tests will accept pass or fail
+                    # lit_not = "not " if unsupported_pair.is_error else ""
+                    lit_not = "not not --crash "
+
+                    prefix_str = SLASH_SLASH
+                    if (
+                        unsupported_pair.driver == VISIBILITY_FLANG
+                        or unsupported_pair.driver == VISIBILITY_FC1
+                    ):
+                        prefix_str = EXCLAMATION
+
+                    CMD_START = f"{prefix_str}RUN: " + lit_not
+
+                    lit_file.write(
+                        CMD_START
+                        + driver_controller[
+                            get_index(unsupported_pair.driver)
+                        ].lit_cmd_prefix
+                        + " "
+                        + unsupported_pair.prefix
+                        + unsupported_pair.option_name
+                        + driver_controller[
+                            get_index(unsupported_pair.driver)
+                        ].shell_cmd_suffix
+                        + driver_controller[
+                            get_index(unsupported_pair.driver)
+                        ].lit_cmd_end
+                        + unsupported_pair.driver
+                        + " %s\n"
+                    )
+
+                # CHECK statements. Instead of writing custom CHECK statements 
for each option-driver pair,
+                # create one statement per driver. Not all options return 
error messages that include their option name
+                for driver in driver_controller:
+                    is_flang_driver = (
+                        driver.visibility_str == VISIBILITY_FLANG
+                        or driver.visibility_str == VISIBILITY_FC1
+                    )
+
+                    if test_visibility == VISIBILITY_FLANG and not 
is_flang_driver:
+                        continue
+                    elif test_visibility == VISIBILITY_DEFAULT and 
is_flang_driver:
+                        continue
+
+                    check_prefix = EXCLAMATION if is_flang_driver else 
SLASH_SLASH
+
+                    lit_file.write(
+                        check_prefix
+                        + driver.visibility_str
+                        + ": "
+                        + driver.check_string
+                        + "\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()
+
+
+# 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 = []
+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__)
+
+# 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"))
+
+# Establish the controller objects for each driver
+driver_cc1as = DriverController(
+    f"{CLANG} {OPTION_CC1AS}",
+    f"{CLANG_LIT} {OPTION_CC1AS}",
+    VISIBILITY_CC1AS,
+    "",
+)
+driver_cc1 = DriverController(
+    f"{CLANG} {OPTION_CC1}",
+    f"{CLANG_LIT} {OPTION_CC1}",
+    VISIBILITY_CC1,
+    " " + OPTION_X + " " + OPTION_CPP,
+)
+driver_cl = DriverController(
+    CLANG_CL,
+    CLANG_CL_LIT,
+    VISIBILITY_CL,
+    " " + OPTION_HASH + " " + OPTION_SLASH_C + " " + OPTION_WX,
+    "{{(unknown argument ignored in|no such file or directory|argument unused 
during compilation)}}",
+    " 2>&1 | FileCheck -check-prefix=",
+)
+driver_dxc = DriverController(
+    CLANG_DXC,
+    CLANG_DXC_LIT,
+    VISIBILITY_DXC,
+    " " + OPTION_HASH + " " + OPTION_T,
+    "{{(unknown argument|no such file or directory|argument unused during 
compilation)}}",
+    " 2>&1 | FileCheck -check-prefix=",
+)
+driver_default = DriverController(
+    CLANG,
+    CLANG_LIT,
+    VISIBILITY_DEFAULT,
+    " " + OPTION_HASH + " " + OPTION_X + " " + OPTION_CPP + " " + OPTION_C,
+    "{{(unknown argument|unsupported option|argument unused|no such file or 
directory)}}",
+)
+driver_fc1 = DriverController(
+    f"{FLANG} {OPTION_FC1}",
+    f"{FLANG_LIT} {OPTION_FC1}",
+    VISIBILITY_FC1,
+    "",
+    "{{(unknown argument|no such file or directory|does not exist)}}",
+)
+# As per flang.f90, "-fc1 is invoked when in --driver-mode=flang",
+# so no point including the below.
+# driver_flang = DriverController(
+#     FLANG,
+#     FLANG_LIT,
+#     VISIBILITY_FLANG,
+#     " " + OPTION_HASH + " " + OPTION_X + " " + OPTION_CPP + " " + OPTION_C,
+#     "{{unknown argument|unsupported option|argument unused during 
compilation|invalid argument|no such file or directory}}",
+# )
+
+driver_controller = [
+    driver_cc1as,
+    driver_cc1,
+    driver_cl,
+    driver_dxc,
+    driver_default,
+    driver_fc1,
+    # driver_flang,
+]
+
+# Gather list of driver flavours
+for visibility in options_json["!instanceof"]["OptionVisibility"]:
+    if visibility == VISIBILITY_FLANG:
+        continue
+    driver_sequence.append(visibility)
+
+# Iterate the options list and find which drivers shouldn't be visible to each 
option
+for option in options_json["!instanceof"]["Option"]:
+    kind = options_json[option]["Kind"]["def"]
+    tmp_visibility_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_name in exceptions_sequence
+        or options_json[option]["Name"] is None
+        or kind == "KIND_INPUT"
+        or kind == "KIND_UNKNOWN"
+    ):
+        untested_sequence.append(
+            UnsupportedDriverOption("All", option, option_name, "")
+        )
+        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]
+
+    get_visibility(option, tmp_visibility_list)
+
+    # Check visibility of direct and indirect aliases
+    # A given option may list only one "primary" alias, but that alias
+    # may be listed by other options as well, hence indirect aliases
+    alias_sequence = options_json["!instanceof"]["Alias"]
+
+    if options_json[option]["Alias"] is not None:
+        primary_alias = options_json[option]["Alias"]["def"]
+
+        get_visibility(primary_alias, tmp_visibility_list)
+
+        for alias in alias_sequence:
+            if options_json[alias]["Alias"]["def"] == primary_alias:
+                get_visibility(alias, tmp_visibility_list)
+
+    for alias in alias_sequence:
+        if options_json[alias]["Alias"]["def"] == option:
+            get_visibility(alias, tmp_visibility_list)
----------------
GeorgeKA wrote:

Ah, just noticing this comment now. Noted. I'll push an update.

https://github.com/llvm/llvm-project/pull/120900
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to