diff --git a/README.md b/README.md index 7feda0c6..7551d863 100644 --- a/README.md +++ b/README.md @@ -1803,6 +1803,38 @@ Default: `1` let g:ycm_echo_current_diagnostic = 1 ``` +### The `g:ycm_filter_diagnostics` option + +This option controls which diagnostics will be rendered by YCM. This option holds +a dictionary of key-values, where the keys are Vim's filetype strings delimited by +commas and values are dictionaries describing the filter. + +A filter is a dictionary of key-values, where the keys are the type of filter, +and the value is a list of arguments to that filter. In the case of just a single +item in the list, you may omit the brackets and just provide the argument directly. +If any filter matches a diagnostic, it will be dropped and YCM will not render it. + +The following filter types are supported: + +- "regex": Accepts a string [regular expression][python-re]. This type matches +when the regex (treated as case-insensitive) is found in the diagnostic text. +- "level": Accepts a string level, either "warning" or "error." This type matches +when the diagnostic has the same level. + +NOTE: The regex syntax is **NOT** Vim's, it's [Python's][python-re]. + +Default: `{}` + +```viml +let g:ycm_filter_diagnostics = { + \ "java": { + \ "regex": [ ".*taco.*", ... ], + \ "level": "error", + \ ... + \ } + \ } +``` + ### The `g:ycm_always_populate_location_list` option When this option is set, YCM will populate the location list automatically every diff --git a/plugin/youcompleteme.vim b/plugin/youcompleteme.vim index ec5399c9..268a7274 100644 --- a/plugin/youcompleteme.vim +++ b/plugin/youcompleteme.vim @@ -121,10 +121,6 @@ let g:ycm_warning_symbol = \ get( g:, 'ycm_warning_symbol', \ get( g:, 'syntastic_warning_symbol', '>>' ) ) -let g:ycm_quiet_messages = - \ get( g:, 'ycm_quiet_messages', - \ get( g:, 'syntastic_quiet_messages', {} ) ) - let g:ycm_goto_buffer_command = \ get( g:, 'ycm_goto_buffer_command', 'same-buffer' ) diff --git a/python/ycm/diagnostic_filter.py b/python/ycm/diagnostic_filter.py index 77de626b..623b9ad0 100644 --- a/python/ycm/diagnostic_filter.py +++ b/python/ycm/diagnostic_filter.py @@ -23,7 +23,7 @@ from future import standard_library standard_library.install_aliases() from builtins import * # noqa -from future.utils import iterkeys +from future.utils import iterkeys, iteritems import re @@ -32,23 +32,18 @@ class DiagnosticFilter( object ): self._filters = [] for filter_type in iterkeys( config ): - wrapper = _AsIs actual_filter_type = filter_type - - if filter_type[ 0 ] == '!': - wrapper = _Not - filter_type = filter_type[ 1 : ] compiler = FILTER_COMPILERS.get( filter_type ) if compiler is not None: for filter_config in _ListOf( config[ actual_filter_type ] ): - fn = wrapper( compiler( filter_config ) ) + fn = compiler( filter_config ) self._filters.append( fn ) def IsAllowed( self, diagnostic ): # NOTE: a diagnostic IsAllowed() ONLY if - # no filters match it + # NO filters match it for f in self._filters: if f( diagnostic ): return False @@ -58,30 +53,34 @@ class DiagnosticFilter( object ): @staticmethod def from_filetype( user_options, filetypes ): - base = dict( user_options.get( 'quiet_messages', {} ) ) + spec = {} + all_filters = dict( user_options.get( 'filter_diagnostics', {} ) ) + for typeSpec, filterValue in iteritems( dict( all_filters ) ): + if typeSpec.find(',') != -1: + for ft in typeSpec.split(','): + all_filters[ ft ] = filterValue for filetype in filetypes: - type_specific = user_options.get( filetype + '_quiet_messages', {} ) - base.update( type_specific ) - return DiagnosticFilter( base ) + type_specific = all_filters.get( filetype, {} ) + spec = _Merge( spec, type_specific ) + return DiagnosticFilter( spec ) def _ListOf( config_entry ): if isinstance( config_entry, list ): return config_entry + if config_entry is None: + return [] + return [ config_entry ] -def _AsIs( fn ): - return fn +def _Merge( into, other ): + for k in iterkeys( other ): + into[k] = _ListOf( into.get( k ) ) + _ListOf( other[ k ] ) - -def _Not( fn ): - def Inverted( diagnostic ): - return not fn( diagnostic ) - - return Inverted + return into def _CompileRegex( raw_regex ): @@ -95,9 +94,9 @@ def _CompileRegex( raw_regex ): def _CompileLevel( level ): # valid kinds are WARNING and ERROR; - # expected input levels are `warnings` and `errors` + # expected input levels are `warning` and `error` # NB: we don't validate the input... - expected_kind = level.upper()[ : -1 ] + expected_kind = level.upper() def FilterLevel( diagnostic ): return diagnostic[ 'kind' ] == expected_kind @@ -105,5 +104,5 @@ def _CompileLevel( level ): return FilterLevel -FILTER_COMPILERS = { 'regex' : _CompileRegex, - 'level' : _CompileLevel } +FILTER_COMPILERS = { 'regex' : _CompileRegex, + 'level' : _CompileLevel } diff --git a/python/ycm/tests/diagnostic_filter_tests.py b/python/ycm/tests/diagnostic_filter_tests.py index 518a973c..3c6109ca 100644 --- a/python/ycm/tests/diagnostic_filter_tests.py +++ b/python/ycm/tests/diagnostic_filter_tests.py @@ -45,45 +45,16 @@ def _assert_rejects( filter, text ): _assert_accept_equals( filter, text, False ) -class ConfigPriority_test(): - - def ConfigPriority_Global_test( self ): - opts = { 'quiet_messages': { 'regex': 'taco' } } - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) - - _assert_rejects( f, 'This is a Taco' ) - _assert_accepts( f, 'This is a Burrito' ) +def _JavaFilter( config ): + return { 'filter_diagnostics' : { 'java': config } } - def ConfigPriority_Filetype_test( self ): - opts = { 'quiet_messages' : {}, - 'java_quiet_messages' : { 'regex': 'taco' } } - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) +def RegexFilter_test(): + opts = _JavaFilter( { 'regex' : 'taco' } ) + f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) - _assert_rejects( f, 'This is a Taco' ) - _assert_accepts( f, 'This is a Burrito' ) - - - def ConfigPriority_FiletypeOverridesGlobal_test( self ): - # NB: if the filetype doesn't override the global, - # we would reject burrito and accept taco - opts = { 'quiet_messages' : { 'regex': 'burrito'}, - 'java_quiet_messages' : { 'regex': 'taco' } } - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) - - _assert_rejects( f, 'This is a Taco' ) - _assert_accepts( f, 'This is a Burrito' ) - - - def ConfigPriority_FiletypeDisablesGlobal_test( self ): - # NB: if the filetype doesn't override the global, - # we would reject burrito and accept taco - opts = { 'quiet_messages' : { 'regex': 'taco'}, - 'java_quiet_messages' : { 'regex': [] } } - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) - - _assert_accepts( f, 'This is a Taco' ) - _assert_accepts( f, 'This is a Burrito' ) + _assert_rejects( f, 'This is a Taco' ) + _assert_accepts( f, 'This is a Burrito' ) class ListOrSingle_test(): @@ -92,7 +63,7 @@ class ListOrSingle_test(): def ListOrSingle_SingleList_test( self ): # NB: if the filetype doesn't override the global, # we would reject burrito and accept taco - opts = { 'quiet_messages' : { 'regex': [ 'taco' ] } } + opts = _JavaFilter( { 'regex' : [ 'taco' ] } ) f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) _assert_rejects( f, 'This is a Taco' ) @@ -102,38 +73,68 @@ class ListOrSingle_test(): def ListOrSingle_MultiList_test( self ): # NB: if the filetype doesn't override the global, # we would reject burrito and accept taco - opts = { 'quiet_messages' : { 'regex': [ 'taco', 'burrito' ] } } + opts = _JavaFilter( { 'regex' : [ 'taco', 'burrito' ] } ) f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) _assert_rejects( f, 'This is a Taco' ) _assert_rejects( f, 'This is a Burrito' ) -def Invert_test(): - opts = { 'quiet_messages' : { '!regex': 'taco' } } - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) - - _assert_accepts( f, 'This is a Taco' ) - _assert_rejects( f, 'This is a Burrito' ) - - class Level_test(): def Level_warnings_test( self ): - opts = { 'quiet_messages' : { 'level': 'warnings' } } + opts = _JavaFilter( { 'level' : 'warning' } ) f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) - _assert_rejects( f, { 'text': 'This is an unimportant taco', - 'kind': 'WARNING' } ) - _assert_accepts( f, { 'text': 'This taco will be shown', - 'kind': 'ERROR' } ) + _assert_rejects( f, { 'text' : 'This is an unimportant taco', + 'kind' : 'WARNING' } ) + _assert_accepts( f, { 'text' : 'This taco will be shown', + 'kind' : 'ERROR' } ) def Level_errors_test( self ): - opts = { 'quiet_messages' : { 'level': 'errors' } } + opts = _JavaFilter( { 'level' : 'error' } ) f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) - _assert_accepts( f, { 'text': 'This is an IMPORTANT taco', - 'kind': 'WARNING' } ) - _assert_rejects( f, { 'text': 'This taco will NOT be shown', - 'kind': 'ERROR' } ) + _assert_accepts( f, { 'text' : 'This is an IMPORTANT taco', + 'kind' : 'WARNING' } ) + _assert_rejects( f, { 'text' : 'This taco will NOT be shown', + 'kind' : 'ERROR' } ) + + +def MultipleFilterTypesTypeTest_test(): + + opts = _JavaFilter( { 'regex' : '.*taco.*', + 'level' : 'warning' } ) + f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) + + _assert_rejects( f, { 'text' : 'This is an unimportant taco', + 'kind' : 'WARNING' } ) + _assert_rejects( f, { 'text' : 'This taco will NOT be shown', + 'kind' : 'ERROR' } ) + _assert_accepts( f, { 'text' : 'This burrito WILL be shown', + 'kind' : 'ERROR' } ) + + +def MergeMultipleFiletypes_test(): + + opts = { 'filter_diagnostics' : { + 'java' : { 'regex' : '.*taco.*' }, + 'xml' : { 'regex' : '.*burrito.*' } } } + + f = DiagnosticFilter.from_filetype( opts, [ 'java', 'xml' ] ) + + _assert_rejects( f, 'This is a Taco' ) + _assert_rejects( f, 'This is a Burrito' ) + _assert_accepts( f, 'This is some Nachos' ) + + +def CommaSeparatedFiletypes_test(): + + opts = { 'filter_diagnostics' : { + 'java,c,cs' : { 'regex' : '.*taco.*' } } } + + f = DiagnosticFilter.from_filetype( opts, [ 'cs' ] ) + + _assert_rejects( f, 'This is a Taco' ) + _assert_accepts( f, 'This is a Burrito' )