add updates libs
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from tclint.cli.utils import make_exclude_filter
|
||||
from tclint.config import Config, load_config_at
|
||||
|
||||
|
||||
class Resolver:
|
||||
def __init__(self, cli_args=None, global_config: Optional[Config] = None):
|
||||
# TODO: Figure out a way to annotate cli_args type effectively.
|
||||
self._config_cache: dict[Path, Config] = {}
|
||||
self._global_config = global_config
|
||||
|
||||
# Instantiate and hang on to a default config so the Resolver can cache many
|
||||
# instances of it without blowing up memory footprint. (This has not been
|
||||
# empirically validated!)
|
||||
self._default_config = Config()
|
||||
self._cli_args = None
|
||||
if cli_args is not None:
|
||||
self._default_config.apply_cli_args(cli_args)
|
||||
self._cli_args = cli_args
|
||||
|
||||
def _find_config(self, directory: Path) -> Config:
|
||||
config = load_config_at(directory)
|
||||
if config is not None:
|
||||
if self._cli_args is not None:
|
||||
config.apply_cli_args(self._cli_args)
|
||||
return config
|
||||
|
||||
# We're at the root, bail!
|
||||
if directory.parent == directory:
|
||||
return self._default_config
|
||||
|
||||
return self.find_config(directory.parent)
|
||||
|
||||
def find_config(self, directory: Path) -> Config:
|
||||
if self._global_config is not None:
|
||||
return self._global_config
|
||||
|
||||
if directory in self._config_cache:
|
||||
return self._config_cache[directory]
|
||||
config = self._find_config(directory)
|
||||
self._config_cache[directory] = config
|
||||
return config
|
||||
|
||||
def resolve_sources(
|
||||
self, paths: list[Path], cwd: Path
|
||||
) -> list[tuple[Optional[Path], Config]]:
|
||||
sources: list[tuple[Optional[Path], Config]] = []
|
||||
|
||||
for path in paths:
|
||||
if str(path) == "-":
|
||||
config = self.find_config(cwd)
|
||||
sources.append((None, config))
|
||||
continue
|
||||
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"path {path} does not exist")
|
||||
|
||||
# We need to slap a .resolve() on the find_config()'s to make sure the
|
||||
# method traverses upwards to the FS root. It would probably ideal to just
|
||||
# resolve each path in the main body of the loop, but this actually has
|
||||
# implications on how the filepath is printed out (it becomes an absolute
|
||||
# path, symlinks are resolved).
|
||||
|
||||
if not path.is_dir():
|
||||
config = self.find_config(path.resolve().parent)
|
||||
is_excluded = make_exclude_filter(config.exclude)
|
||||
if is_excluded(path):
|
||||
continue
|
||||
sources.append((path, config))
|
||||
continue
|
||||
|
||||
for dirstr, dirs, filenames in os.walk(path):
|
||||
dirpath = Path(dirstr)
|
||||
config = self.find_config(dirpath.resolve())
|
||||
is_excluded = make_exclude_filter(config.exclude)
|
||||
extensions = [
|
||||
f".{ext}" if not ext.startswith(".") else ext
|
||||
for ext in config.extensions
|
||||
]
|
||||
|
||||
# Update dirs to prune next directories to traverse based on exclude.
|
||||
to_traverse = []
|
||||
for dir in dirs:
|
||||
if not is_excluded(Path(dir)):
|
||||
to_traverse.append(dir)
|
||||
dirs[:] = to_traverse
|
||||
|
||||
for name in filenames:
|
||||
_, ext = os.path.splitext(name)
|
||||
if ext.lower() in extensions:
|
||||
child = dirpath / name
|
||||
if not is_excluded(child):
|
||||
sources.append((child, config))
|
||||
|
||||
return sources
|
||||
@@ -4,16 +4,17 @@ import argparse
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
from tclint.cli.utils import resolve_sources, register_codec_warning
|
||||
from tclint.cli.resolver import Resolver
|
||||
from tclint.cli.utils import register_codec_warning
|
||||
from tclint.commands.plugins import PluginManager
|
||||
from tclint.config import (
|
||||
get_config,
|
||||
setup_tclfmt_config_cli_args,
|
||||
Config,
|
||||
ConfigError,
|
||||
RunConfig,
|
||||
SpacesInBraces,
|
||||
setup_tclfmt_config_cli_args,
|
||||
)
|
||||
from tclint.parser import Parser, TclSyntaxError
|
||||
from tclint.format import Formatter, FormatterOpts
|
||||
from tclint.parser import Parser, TclSyntaxError
|
||||
|
||||
try:
|
||||
from tclint._version import __version__ # type: ignore
|
||||
@@ -27,22 +28,41 @@ EXIT_SYNTAX_ERROR = 2
|
||||
EXIT_INPUT_ERROR = 4
|
||||
|
||||
|
||||
def format(script: str, config: Config, debug=False) -> str:
|
||||
plugins = [config.commands] if config.commands is not None else []
|
||||
parser = Parser(debug=debug, command_plugins=plugins)
|
||||
def format(
|
||||
script: str,
|
||||
config: Config,
|
||||
plugins: PluginManager,
|
||||
debug=False,
|
||||
debug_whitespace=False,
|
||||
partial=False,
|
||||
) -> str:
|
||||
parser = Parser(debug=debug, commands=plugins.get_commands(config.commands))
|
||||
|
||||
formatter = Formatter(
|
||||
FormatterOpts(
|
||||
indent=config.get_indent(),
|
||||
spaces_in_braces=config.style_spaces_in_braces,
|
||||
indent_mixed_tab_size=config.get_indent_mixed_tab_size(),
|
||||
spaces_in_braces=(
|
||||
config.style_spaces_in_braces == SpacesInBraces.ALWAYS
|
||||
or config.style_spaces_in_braces == SpacesInBraces.BALANCED_YES
|
||||
),
|
||||
balanced_spaces_in_braces=(
|
||||
config.style_spaces_in_braces == SpacesInBraces.BALANCED_NO
|
||||
or config.style_spaces_in_braces == SpacesInBraces.BALANCED_YES
|
||||
),
|
||||
max_blank_lines=config.style_max_blank_lines,
|
||||
indent_namespace_eval=config.style_indent_namespace_eval,
|
||||
emacs=config.style_emacs,
|
||||
debug_whitespace=debug_whitespace,
|
||||
)
|
||||
)
|
||||
return formatter.format_top(script, parser)
|
||||
if partial:
|
||||
return formatter.format_partial(script, parser)
|
||||
else:
|
||||
return formatter.format_top(script, parser)
|
||||
|
||||
|
||||
def check(path: pathlib.Path, script: str, formatted: str):
|
||||
def check(path: str, script: str, formatted: str):
|
||||
parser = Parser()
|
||||
original_tree = parser.parse(script)
|
||||
formatted_tree = parser.parse(formatted)
|
||||
@@ -86,6 +106,12 @@ def main():
|
||||
" of output (e.g. -dd)"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug-whitespace",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="display whitespace in debug mode.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
@@ -94,41 +120,46 @@ def main():
|
||||
default=None,
|
||||
metavar="<path>",
|
||||
)
|
||||
setup_tclfmt_config_cli_args(parser)
|
||||
parser.add_argument(
|
||||
"--partial",
|
||||
help="treat input as a fragment of a script",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
cwd = pathlib.Path.cwd()
|
||||
setup_tclfmt_config_cli_args(parser, cwd)
|
||||
args = parser.parse_args()
|
||||
|
||||
global_config = None
|
||||
if args.config is not None:
|
||||
try:
|
||||
global_config = Config.from_path(args.config, cwd)
|
||||
global_config.apply_cli_args(args)
|
||||
except FileNotFoundError:
|
||||
print(f"Config file path doesn't exist: {args.config}")
|
||||
return EXIT_INPUT_ERROR
|
||||
except ConfigError as e:
|
||||
print(f"Invalid config file: {e}")
|
||||
return EXIT_INPUT_ERROR
|
||||
|
||||
resolver = Resolver(args, global_config)
|
||||
try:
|
||||
config = get_config(args.config, pathlib.Path.cwd())
|
||||
sources = resolver.resolve_sources(args.source, cwd)
|
||||
except FileNotFoundError as e:
|
||||
print(f"Invalid path provided: {e}")
|
||||
return EXIT_INPUT_ERROR
|
||||
except ConfigError as e:
|
||||
print(f"Invalid config file: {e}")
|
||||
return EXIT_INPUT_ERROR
|
||||
|
||||
if config is None:
|
||||
config = RunConfig()
|
||||
|
||||
config.apply_cli_args(args)
|
||||
|
||||
try:
|
||||
# TODO: we should eventually allow tclfmt to find a config by walking up
|
||||
# directories, at which point exclude_root should be the parent dir of
|
||||
# the config file, unless -c is used (eslint rules)
|
||||
exclude_root = pathlib.Path.cwd()
|
||||
sources = resolve_sources(
|
||||
args.source,
|
||||
exclude_patterns=config.exclude,
|
||||
exclude_root=exclude_root,
|
||||
extensions=config.extensions,
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
print(f"Invalid path provided: {e}")
|
||||
return EXIT_INPUT_ERROR
|
||||
plugin_manager = PluginManager(trust_uninstalled=args.trust_plugins)
|
||||
|
||||
retcode = EXIT_OK
|
||||
|
||||
register_codec_warning("replace_with_warning")
|
||||
|
||||
reformat_count = 0
|
||||
for path in sources:
|
||||
for path, config in sources:
|
||||
if path is None:
|
||||
script = sys.stdin.read()
|
||||
out_prefix = "(stdin)"
|
||||
@@ -139,7 +170,12 @@ def main():
|
||||
|
||||
try:
|
||||
formatted = format(
|
||||
script, config.get_for_path(path), debug=(args.debug > 1)
|
||||
script,
|
||||
config,
|
||||
plugin_manager,
|
||||
debug=(args.debug > 1),
|
||||
debug_whitespace=args.debug_whitespace,
|
||||
partial=args.partial,
|
||||
)
|
||||
if args.in_place and path:
|
||||
with open(path, "w") as f:
|
||||
@@ -155,9 +191,15 @@ def main():
|
||||
print(formatted, end="")
|
||||
|
||||
if args.debug > 0:
|
||||
check(path, script, formatted)
|
||||
if args.debug_whitespace:
|
||||
print(
|
||||
"Warning: --debug-whitespace enabled, disabling original vs."
|
||||
" formatted syntax tree check"
|
||||
)
|
||||
else:
|
||||
check(out_prefix, script, formatted)
|
||||
except TclSyntaxError as e:
|
||||
line, col = e.pos
|
||||
line, col = e.start
|
||||
print(f"{out_prefix}:{line}:{col}: syntax error: {e}", file=sys.stderr)
|
||||
retcode |= EXIT_SYNTAX_ERROR
|
||||
continue
|
||||
|
||||
@@ -3,21 +3,16 @@
|
||||
import argparse
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Optional
|
||||
|
||||
|
||||
from tclint.config import (
|
||||
get_config,
|
||||
setup_config_cli_args,
|
||||
Config,
|
||||
ConfigError,
|
||||
RunConfig,
|
||||
)
|
||||
from tclint.parser import Parser, TclSyntaxError
|
||||
from tclint.checks import get_checkers
|
||||
from tclint.violations import Violation, Rule
|
||||
from tclint.cli.resolver import Resolver
|
||||
from tclint.cli.utils import register_codec_warning
|
||||
from tclint.commands.plugins import PluginManager
|
||||
from tclint.comments import CommentVisitor
|
||||
from tclint.cli.utils import resolve_sources, register_codec_warning
|
||||
from tclint.config import Config, ConfigError, setup_config_cli_args
|
||||
from tclint.parser import Parser, TclSyntaxError
|
||||
from tclint.violations import Rule, Violation
|
||||
|
||||
try:
|
||||
from tclint._version import __version__ # type: ignore
|
||||
@@ -32,10 +27,10 @@ EXIT_INPUT_ERROR = 4
|
||||
|
||||
|
||||
def filter_violations(
|
||||
violations: List[Violation],
|
||||
config_ignore: List[Rule],
|
||||
inline_ignore: Dict[int, List[Rule]],
|
||||
) -> List[Violation]:
|
||||
violations: list[Violation],
|
||||
config_ignore: list[Rule],
|
||||
inline_ignore: dict[int, list[Rule]],
|
||||
) -> list[Violation]:
|
||||
filtered_violations = []
|
||||
|
||||
for violation in violations:
|
||||
@@ -53,11 +48,11 @@ def filter_violations(
|
||||
def lint(
|
||||
script: str,
|
||||
config: Config,
|
||||
plugins: PluginManager,
|
||||
path: Optional[pathlib.Path],
|
||||
debug=0,
|
||||
) -> List[Violation]:
|
||||
plugins = [config.commands] if config.commands is not None else []
|
||||
parser = Parser(debug=(debug > 0), command_plugins=plugins)
|
||||
) -> list[Violation]:
|
||||
parser = Parser(debug=(debug > 0), commands=plugins.get_commands(config.commands))
|
||||
|
||||
violations = []
|
||||
tree = parser.parse(script)
|
||||
@@ -66,7 +61,7 @@ def lint(
|
||||
if debug > 0:
|
||||
print(tree.pretty(positions=(debug > 1)))
|
||||
|
||||
for checker in get_checkers():
|
||||
for checker in get_checkers(plugins):
|
||||
violations += checker.check(script, tree, config)
|
||||
|
||||
v = CommentVisitor()
|
||||
@@ -105,40 +100,40 @@ def main():
|
||||
default=None,
|
||||
metavar="<path>",
|
||||
)
|
||||
setup_config_cli_args(parser)
|
||||
|
||||
cwd = pathlib.Path.cwd()
|
||||
setup_config_cli_args(parser, cwd)
|
||||
args = parser.parse_args()
|
||||
|
||||
global_config = None
|
||||
if args.config is not None:
|
||||
try:
|
||||
global_config = Config.from_path(args.config, cwd)
|
||||
global_config.apply_cli_args(args)
|
||||
except FileNotFoundError:
|
||||
print(f"Config file path doesn't exist: {args.config}")
|
||||
return EXIT_INPUT_ERROR
|
||||
except ConfigError as e:
|
||||
print(f"Invalid config file: {e}")
|
||||
return EXIT_INPUT_ERROR
|
||||
|
||||
resolver = Resolver(args, global_config)
|
||||
try:
|
||||
config = get_config(args.config, pathlib.Path())
|
||||
sources = resolver.resolve_sources(args.source, cwd)
|
||||
except FileNotFoundError as e:
|
||||
print(f"Invalid path provided: {e}")
|
||||
return EXIT_INPUT_ERROR
|
||||
except ConfigError as e:
|
||||
print(f"Invalid config file: {e}")
|
||||
return EXIT_INPUT_ERROR
|
||||
|
||||
if config is None:
|
||||
config = RunConfig()
|
||||
|
||||
config.apply_cli_args(args)
|
||||
|
||||
try:
|
||||
# TODO: we should eventually allow tclint to find a config by walking up
|
||||
# directories, at which point exclude_root should be the parent dir of
|
||||
# the config file, unless -c is used (eslint rules)
|
||||
exclude_root = pathlib.Path.cwd()
|
||||
sources = resolve_sources(
|
||||
args.source,
|
||||
exclude_patterns=config.exclude,
|
||||
exclude_root=exclude_root,
|
||||
extensions=config.extensions,
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
print(f"Invalid path provided: {e}")
|
||||
return EXIT_INPUT_ERROR
|
||||
plugin_manager = PluginManager(trust_uninstalled=args.trust_plugins)
|
||||
|
||||
retcode = EXIT_OK
|
||||
|
||||
register_codec_warning("replace_with_warning")
|
||||
|
||||
for path in sources:
|
||||
for path, config in sources:
|
||||
if path is None:
|
||||
script = sys.stdin.read()
|
||||
out_prefix = "(stdin)"
|
||||
@@ -150,7 +145,8 @@ def main():
|
||||
try:
|
||||
violations = lint(
|
||||
script,
|
||||
config.get_for_path(path),
|
||||
config,
|
||||
plugin_manager,
|
||||
path,
|
||||
debug=args.debug,
|
||||
)
|
||||
|
||||
+149
-61
@@ -1,22 +1,27 @@
|
||||
import argparse
|
||||
import dataclasses
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from lsprotocol import types as lsp
|
||||
|
||||
from pygls.server import LanguageServer
|
||||
from pygls.workspace import TextDocument
|
||||
from pygls.uris import to_fs_path
|
||||
from pygls.workspace import TextDocument
|
||||
|
||||
from tclint.cli import tclint
|
||||
from tclint.config import get_config, DEFAULT_CONFIGS, RunConfig, Config, ConfigError
|
||||
from tclint.cli import tclint, utils
|
||||
from tclint.commands.plugins import PluginManager
|
||||
from tclint.config import (
|
||||
DEFAULT_CONFIGS,
|
||||
Config,
|
||||
ConfigError,
|
||||
SpacesInBraces,
|
||||
load_config_at,
|
||||
)
|
||||
from tclint.format import Formatter, FormatterOpts
|
||||
from tclint.lexer import TclSyntaxError
|
||||
from tclint.parser import Parser
|
||||
from tclint.cli import utils
|
||||
|
||||
try:
|
||||
from tclint._version import __version__ # type: ignore
|
||||
@@ -25,13 +30,14 @@ except ModuleNotFoundError:
|
||||
|
||||
|
||||
DIAGNOSTIC_SOURCE = "tclint"
|
||||
_DEFAULT_CONFIG = Config()
|
||||
|
||||
|
||||
def lint(source, config, path):
|
||||
def lint(source, config, plugin_manager, path):
|
||||
diagnostics = []
|
||||
|
||||
try:
|
||||
violations = tclint.lint(source, config, path)
|
||||
violations = tclint.lint(source, config, plugin_manager, path)
|
||||
except TclSyntaxError as e:
|
||||
return [
|
||||
lsp.Diagnostic(
|
||||
@@ -62,7 +68,7 @@ def lint(source, config, path):
|
||||
start=start,
|
||||
end=end,
|
||||
),
|
||||
code=violation.id,
|
||||
code=str(violation.id),
|
||||
source=DIAGNOSTIC_SOURCE,
|
||||
)
|
||||
)
|
||||
@@ -83,15 +89,26 @@ class TclspServer(LanguageServer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.diagnostics = {}
|
||||
self.global_config: RunConfig = None
|
||||
# Maps workspace roots to configs.
|
||||
self.configs: Dict[Path, RunConfig] = {}
|
||||
|
||||
# There are many config caches!!
|
||||
# Caches loaded config files specified in LSP settings.
|
||||
self.workspace_configs: dict[Path, Config] = {}
|
||||
# Caches loaded config files present in open workspaces.
|
||||
self.config_files: dict[Path, Config] = {}
|
||||
# Caches which config is used by each open file.
|
||||
self.source_configs: dict[Path, Config] = {}
|
||||
# Tracks which invalid configs we've already displayed an error for, to avoid
|
||||
# spam.
|
||||
self.invalid_configs: set[Path] = set()
|
||||
|
||||
self.client_supports_refresh = False
|
||||
|
||||
self.global_settings = ExtensionSettings()
|
||||
self.workspace_settings: Dict[Path, ExtensionSettings] = {}
|
||||
self.workspace_settings: dict[Path, ExtensionSettings] = {}
|
||||
|
||||
def get_roots(self) -> List[Path]:
|
||||
self.plugin_manager = PluginManager()
|
||||
|
||||
def get_roots(self) -> list[Path]:
|
||||
"""Returns root folders currently open in the workspace."""
|
||||
roots = []
|
||||
for uri in self.workspace.folders.keys():
|
||||
@@ -108,10 +125,8 @@ class TclspServer(LanguageServer):
|
||||
return roots
|
||||
|
||||
def get_root(self, path: Path) -> Optional[Path]:
|
||||
"""Returns workspace root folder that's closest to path.
|
||||
|
||||
Returns None if path is not in a workspace folder or if there are no workspace
|
||||
folders.
|
||||
"""Returns workspace root if path is in a workspace folder. Otherwise, returns
|
||||
None.
|
||||
"""
|
||||
roots = self.get_roots()
|
||||
closest_root = None
|
||||
@@ -124,55 +139,98 @@ class TclspServer(LanguageServer):
|
||||
if len(relpath.parts) < distance:
|
||||
distance = len(relpath.parts)
|
||||
closest_root = root
|
||||
|
||||
return closest_root
|
||||
|
||||
def get_config_file(self, workspace_root: Path) -> Optional[Path]:
|
||||
if workspace_root in self.workspace_settings:
|
||||
settings = self.workspace_settings[workspace_root]
|
||||
def get_config_file(self, root: Path) -> Optional[Path]:
|
||||
if root in self.workspace_settings:
|
||||
settings = self.workspace_settings[root]
|
||||
return settings.config_file
|
||||
return self.global_settings.config_file
|
||||
|
||||
def load_configs(self):
|
||||
self.configs = {}
|
||||
def show_config_error(self, msg: str, path: Path):
|
||||
if path not in self.invalid_configs:
|
||||
self.show_message(f"Error loading config file: {msg}")
|
||||
self.invalid_configs.add(path)
|
||||
|
||||
def load_config(self, path: Path, root: Path) -> Optional[Config]:
|
||||
try:
|
||||
return Config.from_path(path, root)
|
||||
except FileNotFoundError:
|
||||
self.show_config_error(f"{path} doesn't exist", path)
|
||||
return None
|
||||
except ConfigError as e:
|
||||
self.show_config_error(str(e), path)
|
||||
return None
|
||||
|
||||
def load_workspace_setting_configs(self):
|
||||
"""These may be used a lot if specified, so cache specially."""
|
||||
for root in self.get_roots():
|
||||
try:
|
||||
path = self.get_config_file(root)
|
||||
config = get_config(path, root)
|
||||
path = self.get_config_file(root)
|
||||
if path is None:
|
||||
continue
|
||||
config = self.load_config(path, root)
|
||||
if config is None:
|
||||
continue
|
||||
self.workspace_configs[root] = config
|
||||
|
||||
def _get_config(self, path: Path) -> Config:
|
||||
workspace_root = self.get_root(path)
|
||||
|
||||
# First, check for configs specified in the LSP settings.
|
||||
# If not in a workspace, our only shot is to use a global config. Otherwise, we
|
||||
# bail (no searching, since the LSP only searches up to the workspace root).
|
||||
if workspace_root is None:
|
||||
global_file = self.global_settings.config_file
|
||||
if global_file is not None:
|
||||
config = self.load_config(global_file, path.parent)
|
||||
if config is not None:
|
||||
self.configs[root] = config
|
||||
except ConfigError as e:
|
||||
self.show_message(f"Error loading config file: {e}")
|
||||
return config
|
||||
return _DEFAULT_CONFIG
|
||||
|
||||
# If a global config file exists, we apply it to any file not under a workspace
|
||||
# folder.
|
||||
global_path = self.global_settings.config_file
|
||||
if global_path is not None:
|
||||
# If file is in a workspace, and we've got a workspace config configured, use
|
||||
# that (this logic also handles global configs, since these are still
|
||||
# instantiated once per workspace to resolve relative paths).
|
||||
if workspace_root is not None and workspace_root in self.workspace_configs:
|
||||
return self.workspace_configs[workspace_root]
|
||||
|
||||
# Otherwise, walk upwards until root.
|
||||
# path is a file, which is a sneaky trick to guarantee we always run the first
|
||||
# iteration. It becomes a directory after the first statement in the loop.
|
||||
while path != workspace_root:
|
||||
path = path.parent
|
||||
try:
|
||||
config = get_config(global_path, global_path.parent)
|
||||
self.global_config = config
|
||||
config = load_config_at(path)
|
||||
except ConfigError as e:
|
||||
self.show_message(f"Error loading config file: {e}")
|
||||
self.show_config_error(str(e), path)
|
||||
return _DEFAULT_CONFIG
|
||||
|
||||
def get_config(self, path: Path, root: Optional[Path]) -> Config:
|
||||
if root in self.configs:
|
||||
return self.configs[root].get_for_path(path)
|
||||
if self.global_config is not None:
|
||||
return self.global_config.get_for_path(path)
|
||||
return Config()
|
||||
if config is not None:
|
||||
return config
|
||||
|
||||
def _compute_diagnostics(self, document: TextDocument) -> List[lsp.Diagnostic]:
|
||||
return _DEFAULT_CONFIG
|
||||
|
||||
def get_config(self, path: Path) -> Config:
|
||||
"""Return config object for a given path.
|
||||
|
||||
If no config has already been loaded for root (either by calling this function
|
||||
or load_configs), this function will search for and load a config file if found.
|
||||
"""
|
||||
if path in self.source_configs:
|
||||
return self.source_configs[path]
|
||||
config = self._get_config(path)
|
||||
self.source_configs[path] = config
|
||||
|
||||
return config
|
||||
|
||||
def _compute_diagnostics(self, document: TextDocument) -> list[lsp.Diagnostic]:
|
||||
path = Path(document.path)
|
||||
root = self.get_root(path)
|
||||
config = self.get_config(path, root)
|
||||
|
||||
if root is None:
|
||||
root = path.parent
|
||||
|
||||
config = self.get_config(path)
|
||||
is_excluded = utils.make_exclude_filter(config.exclude)
|
||||
if is_excluded(path, root):
|
||||
if is_excluded(path):
|
||||
return []
|
||||
|
||||
return lint(document.source, config, path)
|
||||
return lint(document.source, config, self.plugin_manager, path)
|
||||
|
||||
def compute_diagnostics(self, document: TextDocument):
|
||||
# `None` sentinel ensures that `diagnostics` gets updated if the URI is not
|
||||
@@ -189,11 +247,10 @@ class TclspServer(LanguageServer):
|
||||
self,
|
||||
document: TextDocument,
|
||||
options: lsp.FormattingOptions,
|
||||
range: Optional[Tuple[int, int]] = None,
|
||||
range: Optional[tuple[int, int]] = None,
|
||||
):
|
||||
path = Path(document.path)
|
||||
root = self.get_root(path)
|
||||
config = self.get_config(path, root)
|
||||
config = self.get_config(path)
|
||||
|
||||
parser = Parser()
|
||||
|
||||
@@ -205,9 +262,19 @@ class TclspServer(LanguageServer):
|
||||
formatter = Formatter(
|
||||
FormatterOpts(
|
||||
indent=indent,
|
||||
spaces_in_braces=config.style_spaces_in_braces,
|
||||
indent_mixed_tab_size=config.get_indent_mixed_tab_size(),
|
||||
spaces_in_braces=(
|
||||
config.style_spaces_in_braces == SpacesInBraces.ALWAYS
|
||||
or config.style_spaces_in_braces == SpacesInBraces.BALANCED_YES
|
||||
),
|
||||
balanced_spaces_in_braces=(
|
||||
config.style_spaces_in_braces == SpacesInBraces.BALANCED_NO
|
||||
or config.style_spaces_in_braces == SpacesInBraces.BALANCED_YES
|
||||
),
|
||||
max_blank_lines=config.style_max_blank_lines,
|
||||
indent_namespace_eval=config.style_indent_namespace_eval,
|
||||
emacs=False,
|
||||
debug_whitespace=False,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -230,13 +297,30 @@ def did_open(ls: TclspServer, params: lsp.DidOpenTextDocumentParams):
|
||||
|
||||
|
||||
@server.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
|
||||
def did_change(ls: TclspServer, params: lsp.DidOpenTextDocumentParams):
|
||||
def did_change(ls: TclspServer, params: lsp.DidChangeTextDocumentParams):
|
||||
"""Parse each document when it is changed"""
|
||||
logging.debug("Received %s: %s", lsp.TEXT_DOCUMENT_DID_CHANGE, params)
|
||||
doc = ls.workspace.get_text_document(params.text_document.uri)
|
||||
ls.compute_diagnostics(doc)
|
||||
|
||||
|
||||
@server.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
|
||||
def did_close(ls: TclspServer, params: lsp.DidCloseTextDocumentParams):
|
||||
"""Free up resources when a document is closed."""
|
||||
logging.debug("Received %s: %s", lsp.TEXT_DOCUMENT_DID_CLOSE, params)
|
||||
doc = ls.workspace.get_text_document(params.text_document.uri)
|
||||
|
||||
try:
|
||||
del ls.diagnostics[doc.uri]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
del ls.source_configs[Path(doc.path)]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
@server.feature(
|
||||
lsp.TEXT_DOCUMENT_DIAGNOSTIC,
|
||||
lsp.DiagnosticOptions(
|
||||
@@ -275,7 +359,11 @@ def change_watched_files(ls: TclspServer, params: lsp.DidChangeWatchedFilesParam
|
||||
# Clear diagnostics cache so they get recalculated when requested
|
||||
ls.diagnostics = {}
|
||||
|
||||
ls.load_configs()
|
||||
# Config files changed, clear the many caches!
|
||||
ls.config_files = {}
|
||||
ls.source_configs = {}
|
||||
ls.invalid_configs = set()
|
||||
|
||||
if ls.client_supports_refresh:
|
||||
ls.lsp.send_request(lsp.WORKSPACE_DIAGNOSTIC_REFRESH, None)
|
||||
|
||||
@@ -369,7 +457,7 @@ def init(ls: TclspServer, params: lsp.InitializeParams):
|
||||
capabilities = ls.client_capabilities.workspace
|
||||
|
||||
try:
|
||||
ls.client_supports_refresh = (
|
||||
ls.client_supports_refresh = bool(
|
||||
capabilities.diagnostics.refresh_support # type: ignore[union-attr]
|
||||
)
|
||||
except AttributeError:
|
||||
@@ -391,7 +479,7 @@ def init(ls: TclspServer, params: lsp.InitializeParams):
|
||||
for settings in (ls.global_settings, *ls.workspace_settings.values()):
|
||||
if settings.config_file is not None:
|
||||
watchers.append(
|
||||
lsp.FileSystemWatcher(glob_pattern=settings.config_file)
|
||||
lsp.FileSystemWatcher(glob_pattern=str(settings.config_file))
|
||||
)
|
||||
|
||||
ls.register_capability(
|
||||
@@ -408,7 +496,7 @@ def init(ls: TclspServer, params: lsp.InitializeParams):
|
||||
)
|
||||
)
|
||||
|
||||
ls.load_configs()
|
||||
ls.load_workspace_setting_configs()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import codecs
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
from typing import List, Optional
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import pathspec
|
||||
|
||||
from tclint.config import ExcludePattern
|
||||
|
||||
|
||||
def register_codec_warning(name):
|
||||
def replace_with_warning_handler(e):
|
||||
@@ -16,73 +19,38 @@ def register_codec_warning(name):
|
||||
codecs.register_error(name, replace_with_warning_handler)
|
||||
|
||||
|
||||
def make_exclude_filter(exclude_patterns: List[str]):
|
||||
exclude_patterns = [
|
||||
re.sub(r"^\s*#", r"\#", pattern) for pattern in exclude_patterns
|
||||
]
|
||||
exclude_spec = pathspec.PathSpec.from_lines("gitwildmatch", exclude_patterns)
|
||||
def make_exclude_filter(
|
||||
exclude_patterns: list[ExcludePattern],
|
||||
) -> Callable[[Path], bool]:
|
||||
# Transform patterns into a data structure keyed on root.
|
||||
patterns_by_root = defaultdict(list)
|
||||
for pattern, root in exclude_patterns:
|
||||
# I think this is escaping #, which would otherwise be treated like a comment.
|
||||
# Not 100% sure though, I originally wrote this a while ago.
|
||||
pattern = re.sub(r"^\s*#", r"\#", pattern)
|
||||
patterns_by_root[root.resolve()].append(pattern)
|
||||
|
||||
def is_excluded(path: pathlib.Path, root: pathlib.Path) -> bool:
|
||||
compiled_patterns = {}
|
||||
for root in patterns_by_root.keys():
|
||||
patterns = patterns_by_root[root]
|
||||
spec = pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
||||
compiled_patterns[root] = spec
|
||||
|
||||
def is_excluded(path: Path) -> bool:
|
||||
abspath = path.resolve()
|
||||
root = root.resolve()
|
||||
|
||||
try:
|
||||
relpath = pathlib.Path(os.path.relpath(abspath, start=root))
|
||||
except ValueError:
|
||||
# We get here if path and exclude_root are on different drives (on Windows).
|
||||
# Things should still behave roughly as expected without using a relative
|
||||
# path. See test_cli_utils.py::test_exclude_filter_windows for test cases.
|
||||
relpath = abspath
|
||||
for root, exclude_spec in compiled_patterns.items():
|
||||
try:
|
||||
relpath = Path(os.path.relpath(abspath, start=root))
|
||||
except ValueError:
|
||||
# We get here if path and exclude_root are on different drives (on
|
||||
# Windows).Things should still behave roughly as expected without
|
||||
# using a relative path. See
|
||||
# test_cli_utils.py::test_exclude_filter_windows for test cases.
|
||||
relpath = abspath
|
||||
|
||||
if exclude_spec.match_file(relpath):
|
||||
return True
|
||||
if exclude_spec.match_file(relpath):
|
||||
return True
|
||||
return False
|
||||
|
||||
return is_excluded
|
||||
|
||||
|
||||
def resolve_sources(
|
||||
paths: List[pathlib.Path],
|
||||
exclude_patterns: List[str],
|
||||
exclude_root: pathlib.Path,
|
||||
extensions: List[str],
|
||||
) -> List[Optional[pathlib.Path]]:
|
||||
"""Resolves paths passed via CLI to a list of filepaths to lint.
|
||||
|
||||
`paths` is a list of paths that may be files or directories. Files are
|
||||
returned verbatim if they exist, and directories are recursively searched
|
||||
for files that have an extension specified in `extensions`. Paths that match a
|
||||
pattern in `exclude_patterns` are ignored (based on gitignore pattern
|
||||
format, see https://git-scm.com/docs/gitignore#_pattern_format).
|
||||
|
||||
Raises FileNotFoundError if a supplied path does not exist.
|
||||
"""
|
||||
extensions = [f".{ext}" if not ext.startswith(".") else ext for ext in extensions]
|
||||
is_excluded = make_exclude_filter(exclude_patterns)
|
||||
|
||||
sources: List[Optional[pathlib.Path]] = []
|
||||
|
||||
for path in paths:
|
||||
if str(path) == "-":
|
||||
sources.append(None)
|
||||
continue
|
||||
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"path {path} does not exist")
|
||||
|
||||
if is_excluded(path, exclude_root):
|
||||
continue
|
||||
|
||||
if not path.is_dir():
|
||||
sources.append(path)
|
||||
continue
|
||||
|
||||
for dirpath, _, filenames in os.walk(path):
|
||||
for name in filenames:
|
||||
_, ext = os.path.splitext(name)
|
||||
if ext.lower() in extensions:
|
||||
child = pathlib.Path(dirpath) / name
|
||||
if not is_excluded(child, exclude_root):
|
||||
sources.append(child)
|
||||
|
||||
return sources
|
||||
|
||||
Reference in New Issue
Block a user