Skip to content

Commit f75e51d

Browse files
committed
refactor: improve extra-lint-rules feature based on review feedback
1 parent cd68913 commit f75e51d

File tree

2 files changed

+168
-218
lines changed

2 files changed

+168
-218
lines changed

samcli/commands/validate/validate.py

+155-118
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,13 @@ class SamTemplate:
5757
"Create a cfnlintrc config file to specify additional parameters. "
5858
"For more information, see: https://github.com/aws-cloudformation/cfn-lint",
5959
)
60-
@click.option(
61-
"--serverless-rules",
62-
is_flag=True,
63-
help="[DEPRECATED] Enable Serverless Rules for linting validation. "
64-
"Requires the cfn-lint-serverless package to be installed. "
65-
"Use --extra-lint-rules=\"cfn_lint_serverless.rules\" instead. "
66-
"For more information, see: https://github.com/awslabs/serverless-rules",
67-
)
6860
@click.option(
6961
"--extra-lint-rules",
7062
help="Specify additional lint rules to be used with cfn-lint. "
71-
"Format: module.path (e.g. 'cfn_lint_serverless.rules')",
72-
default=None
63+
"Format: module.path (e.g. 'cfn_lint_serverless.rules'). "
64+
"Multiple rule modules can be specified by separating with commas or using this option multiple times.",
65+
default=None,
66+
multiple=True
7367
)
7468
@save_params_option
7569
@pass_context
@@ -78,22 +72,12 @@ class SamTemplate:
7872
@print_cmdline_args
7973
@unsupported_command_cdk(alternative_command="cdk doctor")
8074
@command_exception_handler
81-
def cli(ctx, template_file, config_file, config_env, lint, save_params, serverless_rules, extra_lint_rules):
75+
def cli(ctx, template_file, config_file, config_env, lint, save_params, extra_lint_rules):
8276
# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing
83-
84-
# Show warning and convert to extra_lint_rules if serverless_rules is used
85-
if serverless_rules and not extra_lint_rules:
86-
click.secho(
87-
"Warning: --serverless-rules is deprecated. Please use --extra-lint-rules=\"cfn_lint_serverless.rules\" instead.",
88-
fg="yellow"
89-
)
90-
# Convert old option to new option
91-
extra_lint_rules = "cfn_lint_serverless.rules"
92-
93-
do_cli(ctx, template_file, lint, serverless_rules, extra_lint_rules) # pragma: no cover
77+
do_cli(ctx, template_file, lint, extra_lint_rules) # pragma: no cover
9478

9579

96-
def do_cli(ctx, template, lint, serverless_rules, extra_lint_rules=None):
80+
def do_cli(ctx, template, lint, extra_lint_rules=None):
9781
"""
9882
Implementation of the ``cli`` method, just separated out for unit testing purposes
9983
"""
@@ -107,7 +91,7 @@ def do_cli(ctx, template, lint, serverless_rules, extra_lint_rules=None):
10791
sam_template = _read_sam_file(template)
10892

10993
if lint:
110-
_lint(ctx, sam_template.serialized, template, serverless_rules, extra_lint_rules)
94+
_lint(ctx, sam_template.serialized, template, extra_lint_rules)
11195
else:
11296
iam_client = boto3.client("iam")
11397
validator = SamTemplateValidator(
@@ -159,137 +143,190 @@ def _read_sam_file(template) -> SamTemplate:
159143
return SamTemplate(serialized=template_string, deserialized=sam_template)
160144

161145

162-
def _lint(ctx: Context, template: str, template_path: str, serverless_rules: bool = False, extra_lint_rules: str = None) -> None:
146+
def _lint(ctx: Context, template: str, template_path: str, extra_lint_rules=None):
163147
"""
164148
Parses provided SAM template and maps errors from CloudFormation template back to SAM template.
165149
166150
Cfn-lint loggers are added to the SAM cli logging hierarchy which at the root logger
167-
configures with INFO level logging and a different formatting. This exposes and duplicates
168-
some cfn-lint logs that are not typically shown to customers. Explicitly setting the level to
169-
WARNING and propagate to be False remediates these issues.
151+
formatter and handlers are defined. This ensures that logging is output correctly when used from SAM cli
152+
for CLI consumers.
170153
171154
Parameters
172-
-----------
155+
----------
173156
ctx
174-
Click context object
157+
Click Context
175158
template
176-
Contents of sam template as a string
159+
SAM template contents
177160
template_path
178161
Path to the sam template
179-
serverless_rules
180-
Flag to enable Serverless Rules for linting
162+
extra_lint_rules
163+
List of additional rule modules to apply
181164
"""
165+
import logging
166+
import importlib.util
167+
import cfnlint
182168

183-
from cfnlint.api import ManualArgs, lint
169+
from cfnlint.api import lint, ManualArgs
184170
from cfnlint.runner import InvalidRegionException
171+
# Import only what is necessary
172+
173+
# To track events, we need to enable telemetry
185174
from samcli.lib.telemetry.event import EventTracker
186175

187-
# Add debug information
188-
print(f"Debug info: serverless_rules option value = {serverless_rules}")
176+
LOG = logging.getLogger(__name__)
177+
LOG.debug("Starting template validation with linting")
189178

190-
cfn_lint_logger = logging.getLogger(CNT_LINT_LOGGER_NAME)
191-
cfn_lint_logger.propagate = False
179+
# Set up cfnlint logger verbosity using context provided
180+
cfnlint_logger = logging.getLogger(CNT_LINT_LOGGER_NAME)
181+
cfnlint_logger.propagate = False
182+
183+
if ctx and ctx.debug:
184+
cfnlint_logger.propagate = True
185+
cfnlint_logger.setLevel(logging.DEBUG)
186+
else:
187+
cfnlint_logger.setLevel(logging.INFO)
192188

189+
# Track linting in telemetry
193190
EventTracker.track_event("UsedFeature", "CFNLint")
194-
191+
192+
# Create linter configuration
195193
linter_config = {}
196194
if ctx.region:
197195
linter_config["regions"] = [ctx.region]
198-
if ctx.debug:
199-
cfn_lint_logger.propagate = True
200-
cfn_lint_logger.setLevel(logging.DEBUG)
201-
202-
print(f"Debug info: initial linter_config = {linter_config}")
203-
204-
# Initialize variable to handle both options together
205-
rules_to_append = []
206196

207-
# Support for previous serverless_rules option (deprecated)
208-
if serverless_rules:
209-
print("Debug info: serverless_rules option is activated.")
210-
# Track usage of Serverless Rules
211-
EventTracker.track_event("UsedFeature", "ServerlessRules")
212-
213-
# Check if cfn-lint-serverless is installed
214-
import importlib.util
215-
serverless_spec = importlib.util.find_spec("cfn_lint_serverless")
216-
print(f"Debug info: cfn_lint_serverless package installed = {serverless_spec is not None}")
217-
218-
if serverless_spec is None:
219-
print("Debug info: cfn_lint_serverless package is not installed.")
220-
click.secho(
221-
"Serverless Rules package (cfn-lint-serverless) is not installed. "
222-
"Please install it using: pip install cfn-lint-serverless",
223-
fg="red",
224-
)
225-
raise UserException(
226-
"Serverless Rules package (cfn-lint-serverless) is not installed. "
227-
"Please install it using: pip install cfn-lint-serverless"
228-
)
229-
230-
try:
231-
# Try to import the package
232-
import cfn_lint_serverless
233-
print("Debug info: cfn_lint_serverless package import successful")
234-
235-
# Add Serverless Rules to the rule list
236-
rules_to_append.append("cfn_lint_serverless.rules")
237-
click.secho("Serverless Rules enabled for linting", fg="green")
238-
except ImportError as e:
239-
print(f"Debug info: cfn_lint_serverless import error = {e}")
240-
click.secho(
241-
"Serverless Rules package (cfn-lint-serverless) is not installed. "
242-
"Please install it using: pip install cfn-lint-serverless",
243-
fg="red",
244-
)
245-
raise UserException(
246-
"Serverless Rules package (cfn-lint-serverless) is not installed. "
247-
"Please install it using: pip install cfn-lint-serverless"
248-
)
249-
250-
# Support for the new extra_lint_rules option
197+
# Process extra lint rules if provided
198+
rules_to_append = []
251199
if extra_lint_rules:
252-
print(f"Debug info: extra_lint_rules option is activated. Value: {extra_lint_rules}")
253200
# Track usage of Extra Lint Rules
254201
EventTracker.track_event("UsedFeature", "ExtraLintRules")
255202

256-
# Parse comma-separated rule modules
257-
modules = [module.strip() for module in extra_lint_rules.split(',') if module.strip()]
258-
print(f"Debug info: parsed rule modules list = {modules}")
203+
# Process each rule option (multiple=True gives us a list)
204+
for rule_option in extra_lint_rules:
205+
# Handle comma-separated rule modules
206+
for module in rule_option.split(','):
207+
module = module.strip()
208+
if not module:
209+
continue
210+
211+
LOG.debug("Processing lint rule module: %s", module)
212+
if _is_module_available(module):
213+
rules_to_append.append(module)
214+
LOG.debug("Module %s is available and will be used", module)
215+
else:
216+
module_name = module.split('.')[0].replace('_', '-')
217+
_handle_missing_module(module_name,
218+
f"The rule module '{module}' was specified but is not available.",
219+
ctx.debug)
259220

260-
# Add each module to the rule list
261-
rules_to_append.extend(modules)
262-
click.secho(f"Extra lint rules enabled: {extra_lint_rules}", fg="green")
263-
264-
# Add rules to linter_config if any exist
265-
if rules_to_append:
266-
print(f"Debug info: rules to append = {rules_to_append}")
267-
linter_config["append_rules"] = rules_to_append
268-
print(f"Debug info: updated linter_config = {linter_config}")
269-
270-
config = ManualArgs(**linter_config)
271-
print(f"Debug info: config creation completed")
221+
if rules_to_append:
222+
module_names = ', '.join(rules_to_append)
223+
click.secho(f"Extra lint rules enabled: {module_names}", fg="green")
224+
linter_config["append_rules"] = rules_to_append
225+
LOG.debug("Linter configuration updated with rules: %s", rules_to_append)
272226

273227
try:
274-
print(f"Debug info: starting lint function call")
228+
# Create linter configuration and execute linting
229+
config = ManualArgs(**linter_config)
230+
LOG.debug("Executing linting with configuration")
275231
matches = lint(template, config=config)
276-
print(f"Debug info: lint function call completed, matches = {matches}")
232+
233+
if not matches:
234+
click.secho("{} is a valid SAM Template".format(template_path), fg="green")
235+
return
236+
237+
# Display validation failures
238+
click.secho(matches)
239+
raise LinterRuleMatchedException("Linting failed. At least one linting rule was matched to the provided template.")
240+
277241
except InvalidRegionException as ex:
278-
print(f"Debug info: InvalidRegionException occurred = {ex}")
242+
LOG.debug("Region validation failed: %s", ex)
279243
raise UserException(
280244
f"AWS Region was not found. Please configure your region through the --region option.\n{ex}",
281245
wrapped_from=ex.__class__.__name__,
282246
) from ex
283247
except Exception as e:
284-
print(f"Debug info: exception occurred = {e}")
248+
LOG.debug("Unexpected exception during linting: %s", e)
285249
raise
286250

287-
if not matches:
288-
print(f"Debug info: template validation successful")
289-
click.secho("{} is a valid SAM Template".format(template_path), fg="green")
290-
return
291251

292-
print(f"Debug info: template validation failed, matches = {matches}")
293-
click.secho(matches)
252+
def _is_module_available(module_path: str) -> bool:
253+
"""
254+
Check if a module is available for import.
255+
Works with both standard pip installations and installer-based SAM CLI.
256+
257+
Parameters
258+
----------
259+
module_path
260+
Full module path (e.g. 'cfn_lint_serverless.rules')
261+
262+
Returns
263+
-------
264+
bool
265+
True if module can be imported, False otherwise
266+
"""
267+
LOG = logging.getLogger(__name__)
268+
269+
# Try using importlib.util which is safer
270+
try:
271+
root_module = module_path.split('.')[0]
272+
spec = importlib.util.find_spec(root_module)
273+
if spec is None:
274+
LOG.debug("Module %s not found with importlib.util.find_spec", root_module)
275+
return False
276+
277+
# For deeper paths, try actually importing
278+
try:
279+
__import__(module_path)
280+
return True
281+
except (ImportError, ModuleNotFoundError) as e:
282+
LOG.debug("Could not import module %s: %s", module_path, e)
283+
return False
284+
except Exception as e:
285+
LOG.debug("Unexpected error checking for module %s: %s", module_path, e)
286+
# Fallback to direct import attempt
287+
try:
288+
__import__(module_path)
289+
return True
290+
except (ImportError, ModuleNotFoundError):
291+
return False
292+
294293

295-
raise LinterRuleMatchedException("Linting failed. At least one linting rule was matched to the provided template.")
294+
def _handle_missing_module(package_name: str, error_context: str, debug_mode: bool = False):
295+
"""
296+
Handle missing module by providing appropriate error message that works
297+
in both pip and installer environments.
298+
299+
Parameters
300+
----------
301+
package_name
302+
Name of the package (for pip install instructions)
303+
error_context
304+
Contextual message describing what feature requires this package
305+
debug_mode
306+
Whether to include detailed instructions for different install methods
307+
308+
Raises
309+
------
310+
UserException
311+
With appropriate error message
312+
"""
313+
LOG = logging.getLogger(__name__)
314+
LOG.debug("Module %s is missing: %s", package_name, error_context)
315+
316+
base_message = error_context
317+
install_instruction = f"Please install it using: pip install {package_name}"
318+
319+
if debug_mode:
320+
# In debug mode, provide more comprehensive instructions
321+
message = (
322+
f"{base_message}\n\n"
323+
f"The package '{package_name}' is not available. Installation options:\n"
324+
f"1. If using pip-installed SAM CLI: {install_instruction}\n"
325+
f"2. If using installer-based SAM CLI: You need to install the package in the same Python environment\n"
326+
f" that SAM CLI uses. Check the SAM CLI installation documentation for details."
327+
)
328+
else:
329+
message = f"{base_message}\n\n{package_name} package is not installed. {install_instruction}"
330+
331+
click.secho(message, fg="red")
332+
raise UserException(message)

0 commit comments

Comments
 (0)