Replace optional Tools fields (using `hasattr`) with normal class fields.

Also remove `version_filter` completely.  The last usage of this
field was removed in #1165.
diff --git a/emsdk.py b/emsdk.py
index 06f9864..f6272ad 100644
--- a/emsdk.py
+++ b/emsdk.py
@@ -870,7 +870,7 @@
   generator_suffix = cmake_generator_prefix()
   bitness_suffix = '_32' if tool.bitness == 32 else '_64'
 
-  if hasattr(tool, 'git_branch'):
+  if tool.git_branch:
     build_dir = 'build_' + tool.git_branch.replace(os.sep, '-') + generator_suffix + bitness_suffix
   else:
     build_dir = 'build_' + tool.version + generator_suffix + bitness_suffix
@@ -1064,7 +1064,7 @@
 
 def cmake_target_platform(tool):
   # Source: https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2017%202022.html#platform-selection
-  if hasattr(tool, 'arch'):
+  if tool.arch:
     if tool.arch == 'arm64':
       return 'ARM64'
     elif tool.arch == 'x86_64':
@@ -1578,14 +1578,14 @@
 def get_required_path(active_tools):
   path_add = [to_native_path(EMSDK_PATH)]
   for tool in active_tools:
-    if hasattr(tool, 'activated_path'):
+    if tool.activated_path:
       path = to_native_path(tool.expand_vars(tool.activated_path))
       # If the tool has an activated_path_skip attribute then we don't add
       # the tools path to the users path if a program by that name is found
       # in the existing PATH.  This allows us to, for example, add our version
       # node to the users PATH if, and only if, they don't already have a
       # another version of node in their PATH.
-      if hasattr(tool, 'activated_path_skip'):
+      if tool.activated_path_skip:
         current_path = shutil.which(tool.activated_path_skip)
         # We found an executable by this name in the current PATH, but we
         # ignore our own version for this purpose.
@@ -1803,7 +1803,28 @@
 
 
 class Tool:
+  os = None
+  bitness = None
+  append_bitness = True
+  is_old = False
+  version = None
+  activated_path = None
+  cmake_build_type = None
+  install_path = None
+  activated_path_skip = False
+  activated_cfg = None
+  activated_env = None
+  arch = None
+  url = None
+  custom_is_installed_script = None
+  custom_install_script = None
+  custom_uninstall_script = None
+  emscripten_releases_hash = None
+  git_branch = None
+
   def __init__(self, data):
+    self.uses = []
+
     # Convert the dictionary representation of the tool in 'data' to members of
     # this class for convenience.
     for key, value in data.items():
@@ -1813,7 +1834,7 @@
     self.name = self.id
     if self.version:
       self.name += '-' + self.version
-    if hasattr(self, 'bitness'):
+    if self.bitness:
       self.name += '-' + str(self.bitness) + 'bit'
 
   def __str__(self):
@@ -1848,29 +1869,28 @@
 
   # Return true if this tool requires building from source, and false if this is a precompiled tool.
   def needs_compilation(self):
-    if hasattr(self, 'cmake_build_type'):
+    if self.cmake_build_type:
       return True
 
-    if hasattr(self, 'uses'):
-      for tool_name in self.uses:
-        tool = find_tool(tool_name)
-        if not tool:
-          debug_print(f'Tool {self} depends on {tool_name} which does not exist!')
-          continue
-        if tool.needs_compilation():
-          return True
+    for tool_name in self.uses:
+      tool = find_tool(tool_name)
+      if not tool:
+        debug_print(f'Tool {self} depends on {tool_name} which does not exist!')
+        continue
+      if tool.needs_compilation():
+        return True
 
     return False
 
   # Specifies the target path where this tool will be installed to. This could
   # either be a directory or a filename (e.g. in case of node.js)
   def installation_path(self):
-    if hasattr(self, 'install_path'):
+    if self.install_path:
       pth = self.expand_vars(self.install_path)
       return sdk_path(pth)
     p = self.version
-    if hasattr(self, 'bitness') and (not hasattr(self, 'append_bitness') or self.append_bitness):
-      p += '_' + str(self.bitness) + 'bit'
+    if self.bitness and self.append_bitness:
+      p += f'_{self.bitness}bit'
     return sdk_path(os.path.join(self.id, p))
 
   # Specifies the target directory this tool will be installed to.
@@ -1884,34 +1904,29 @@
   # Returns the configuration item that needs to be added to .emscripten to make
   # this Tool active for the current user.
   def activated_config(self):
-    if hasattr(self, 'activated_cfg'):
-      activated_cfg = self.activated_cfg
-    else:
+    if not self.activated_cfg:
       return {}
 
     config = OrderedDict()
-    expanded = to_unix_path(self.expand_vars(activated_cfg))
+    expanded = to_unix_path(self.expand_vars(self.activated_cfg))
     for specific_cfg in expanded.split(';'):
       name, value = specific_cfg.split('=')
       config[name] = value.strip("'")
     return config
 
   def activated_environment(self):
-    if hasattr(self, 'activated_env'):
-      activated_env = self.activated_env
-    else:
+    if not self.activated_env:
       return []
 
-    return self.expand_vars(activated_env).split(';')
+    return self.expand_vars(self.activated_env).split(';')
 
   def compatible_with_this_arch(self):
-    if hasattr(self, 'arch'):
-      if self.arch != ARCH:
-        return False
+    if self.arch and self.arch != ARCH:
+      return False
     return True
 
   def compatible_with_this_os(self):
-    if hasattr(self, 'os'):
+    if self.os:
       if self.os == 'all':
         return True
       if self.compatible_with_this_arch() and ((WINDOWS and 'win' in self.os) or (LINUX and ('linux' in self.os or 'unix' in self.os)) or (MACOS and ('macos' in self.os or 'unix' in self.os))):
@@ -1919,22 +1934,22 @@
       else:
         return False
     else:
-      if not any(hasattr(self, a) for a in ('macos_url', 'windows_url', 'unix_url', 'linux_url')):
+      if not any((self.macos_url, self.windows_url, self.unix_url, self.linux_url)):
         return True
 
-    if MACOS and hasattr(self, 'macos_url') and self.compatible_with_this_arch():
+    if MACOS and self.macos_url and self.compatible_with_this_arch():
       return True
 
-    if LINUX and hasattr(self, 'linux_url') and self.compatible_with_this_arch():
+    if LINUX and self.linux_url and self.compatible_with_this_arch():
       return True
 
-    if WINDOWS and hasattr(self, 'windows_url') and self.compatible_with_this_arch():
+    if WINDOWS and self.windows_url and self.compatible_with_this_arch():
       return True
 
-    if UNIX and hasattr(self, 'unix_url'):
+    if UNIX and self.unix_url:
       return True
 
-    return hasattr(self, 'url')
+    return self.url is not None
 
   # the "version file" is a file inside install dirs that indicates the
   # version installed there. this helps disambiguate when there is more than
@@ -1958,7 +1973,7 @@
   def is_installed(self, skip_version_check=False):
     # If this tool/sdk depends on other tools, require that all dependencies are
     # installed for this tool to count as being installed.
-    if hasattr(self, 'uses'):
+    if self.uses:
       for tool_name in self.uses:
         tool = find_tool(tool_name)
         if tool is None:
@@ -1981,10 +1996,10 @@
     # clang-main-64bit, clang-main-32bit and clang-main-64bit each
     # share the same git repo), require that in addition to the installation
     # directory, each item in the activated PATH must exist.
-    if hasattr(self, 'activated_path') and not os.path.exists(self.expand_vars(self.activated_path)):
+    if self.activated_path and not os.path.exists(self.expand_vars(self.activated_path)):
       content_exists = False
 
-    if hasattr(self, 'custom_is_installed_script'):
+    if self.custom_is_installed_script:
       if self.custom_is_installed_script == 'is_binaryen_installed':
         return is_binaryen_installed(self)
       elif self.custom_is_installed_script == 'is_firefox_installed':
@@ -2031,7 +2046,7 @@
         debug_print(f'{self} is not active, because environment variable key="{key}" has value "{os.getenv(key)}" but should have value "{value}"')
         return False
 
-    if hasattr(self, 'activated_path'):
+    if self.activated_path:
       path = to_unix_path(self.expand_vars(self.activated_path))
       for p in path:
         path_items = os.environ['PATH'].replace('\\', '/').split(ENVPATH_SEPARATOR)
@@ -2044,24 +2059,20 @@
   # Otherwise, this function returns a string that describes the reason why this
   # tool is not available.
   def can_be_installed(self):
-    if hasattr(self, 'bitness'):
-      if self.bitness == 64 and not is_os_64bit():
+    if self.bitness == 64 and not is_os_64bit():
         return "this tool is only provided for 64-bit OSes"
     return True
 
   def download_url(self):
-    if WINDOWS and hasattr(self, 'windows_url'):
+    if WINDOWS and self.windows_url:
       return self.windows_url
-    elif MACOS and hasattr(self, 'macos_url'):
+    elif MACOS and self.macos_url:
       return self.macos_url
-    elif LINUX and hasattr(self, 'linux_url'):
+    elif LINUX and self.linux_url:
       return self.linux_url
-    elif UNIX and hasattr(self, 'unix_url'):
+    elif UNIX and self.unix_url:
       return self.unix_url
-    elif hasattr(self, 'url'):
-      return self.url
-    else:
-      return None
+    return self.url
 
   def install(self):
     """Returns True if the Tool was installed of False if was skipped due to
@@ -2092,7 +2103,7 @@
       print(f"All SDK components already installed: '{self}'.")
       return False
 
-    if getattr(self, 'custom_install_script', None) == 'emscripten_npm_install':
+    if self.custom_install_script == 'emscripten_npm_install':
       # upstream tools have hardcoded paths that are not stored in emsdk_manifest.json registry
       install_path = 'upstream'
       emscripten_dir = os.path.join(EMSDK_PATH, install_path, 'emscripten')
@@ -2113,7 +2124,7 @@
     # However all tools that are sourced directly from git branches do need to be
     # installed every time when requested, since the install step is then used to git
     # pull the tool to a newer version.
-    if self.is_installed() and not hasattr(self, 'git_branch'):
+    if self.is_installed() and not self.git_branch:
       print(f"Skipped installing {self.name}, already installed.")
       return False
 
@@ -2127,9 +2138,9 @@
       'download_node_nightly': download_node_nightly,
       'download_firefox': download_firefox,
     }
-    if hasattr(self, 'custom_install_script') and self.custom_install_script in custom_install_scripts:
+    if self.custom_install_script in custom_install_scripts:
       success = custom_install_scripts[self.custom_install_script](self)
-    elif hasattr(self, 'git_branch'):
+    elif self.git_branch:
       success = git_clone_checkout_and_pull(url, self.installation_path(), self.git_branch, getattr(self, 'remote_name', 'origin'))
     elif url.endswith(ARCHIVE_SUFFIXES):
       success = download_and_extract(url, self.installation_path(),
@@ -2140,7 +2151,7 @@
     if not success:
       exit_with_error("installation failed!")
 
-    if hasattr(self, 'custom_install_script'):
+    if self.custom_install_script:
       if self.custom_install_script == 'emscripten_npm_install':
         success = emscripten_npm_install(self, self.installation_path())
       elif self.custom_install_script in {'build_llvm', 'build_ninja', 'build_ccache', 'download_node_nightly', 'download_firefox'}:
@@ -2158,7 +2169,7 @@
     # Install an emscripten-version.txt file if told to, and if there is one.
     # (If this is not an actual release, but some other build, then we do not
     # write anything.)
-    if hasattr(self, 'emscripten_releases_hash'):
+    if self.emscripten_releases_hash:
       emscripten_version_file_path = os.path.join(to_native_path(self.expand_vars(self.activated_path)), 'emscripten-version.txt')
       version = get_emscripten_release_version(self.emscripten_releases_hash)
       if version:
@@ -2190,7 +2201,7 @@
       print(f"Tool '{self}' was not installed. No need to uninstall.")
       return
     print(f"Uninstalling tool '{self}'..")
-    if hasattr(self, 'custom_uninstall_script'):
+    if self.custom_uninstall_script:
       if self.custom_uninstall_script == 'uninstall_binaryen':
         uninstall_binaryen(self)
       else:
@@ -2200,8 +2211,6 @@
     print(f"Done uninstalling '{self}'.")
 
   def dependencies(self):
-    if not hasattr(self, 'uses'):
-      return []
     deps = []
 
     for tool_name in self.uses:
@@ -2211,8 +2220,6 @@
     return deps
 
   def recursive_dependencies(self):
-    if not hasattr(self, 'uses'):
-      return []
     deps = []
     for tool_name in self.uses:
       tool = find_tool(tool_name)
@@ -2512,14 +2519,7 @@
       if not found_param:
         continue
       t2.is_old = i < len(category_list) - 2
-      if hasattr(t2, 'uses'):
-        t2.uses = [x.replace(param, ver) for x in t2.uses]
-
-      # Filter out expanded tools by version requirements, such as ["tag", "<=", "1.37.22"]
-      if hasattr(t2, 'version_filter'):
-        passes = passes_filters(param, ver, t2.version_filter)
-        if not passes:
-          continue
+      t2.uses = [x.replace(param, ver) for x in t2.uses]
 
       if is_sdk:
         if dependencies_exist(t2):
@@ -2536,9 +2536,6 @@
   for tool in manifest['tools']:
     t = Tool(tool)
     if t.compatible_with_this_os():
-      if not hasattr(t, 'is_old'):
-        t.is_old = False
-
       # Expand the metapackages that refer to tags
       if '%tag%' in t.version:
         expand_category_param('%tag%', emscripten_tags, t, is_sdk=False)