diff --git a/.sg/rules/os-environ-usage.yml b/.sg/rules/os-environ-usage.yml new file mode 100644 index 00000000000..df2c0386a67 --- /dev/null +++ b/.sg/rules/os-environ-usage.yml @@ -0,0 +1,93 @@ +id: os-environ-usage +message: Use `ddtrace.settings._env` instead of `os` to access environment variables +severity: error +language: python +files: + - "ddtrace/**" +ignores: + - "ddtrace/settings/_env.py" +rule: + any: + # Match os.environ access patterns + - pattern: os.environ + - pattern: os.environ[$$$ARGS] + - pattern: os.environ.get($$$ARGS) + - pattern: os.environ.keys($$$ARGS) + - pattern: os.environ.values($$$ARGS) + - pattern: os.environ.items($$$ARGS) + - pattern: os.environ.copy($$$ARGS) + - pattern: os.environ.clear($$$ARGS) + - pattern: os.environ.update($$$ARGS) + - pattern: os.environ.pop($$$ARGS) + - pattern: os.environ.setdefault($$$ARGS) + + # Match os.getenv() calls + - pattern: os.getenv($$$ARGS) + + # Match direct imports and usage of environ + - pattern: from os import environ + - pattern: from os import environ as $ALIAS + - pattern: from os import $$$IMPORTS, environ + - pattern: from os import $$$IMPORTS, environ as $ALIAS + - pattern: from os import environ, $$$IMPORTS + - pattern: from os import environ as $ALIAS, $$$IMPORTS + + # Match direct imports and usage of getenv + - pattern: from os import getenv + - pattern: from os import getenv as $ALIAS + - pattern: from os import $$$IMPORTS, getenv + - pattern: from os import $$$IMPORTS, getenv as $ALIAS + - pattern: from os import getenv, $$$IMPORTS + - pattern: from os import getenv as $ALIAS, $$$IMPORTS + + # Match usage of imported environ (direct name access) + - pattern: environ[$$$ARGS] + has: + kind: module + pattern: | + from os import environ + - pattern: environ.get($$$ARGS) + has: + kind: module + pattern: | + from os import environ + - pattern: environ.keys($$$ARGS) + has: + kind: module + pattern: | + from os import environ + - pattern: environ.values($$$ARGS) + has: + kind: module + pattern: | + from os import environ + - pattern: environ.items($$$ARGS) + has: + kind: module + pattern: | + from os import environ + + # Match usage of imported getenv + - pattern: getenv($$$ARGS) + has: + kind: module + pattern: | + from os import getenv + +note: | + Direct access to os.environ or os.getenv is not allowed in this codebase. + + Instead, use the centralized environment variable helper `ddtrace.settings._env` + + Before: + import os + value = os.environ["HOME"] + debug = os.getenv("DEBUG", "false") + + After: + from ddtrace.settings import _env + value = _env.environ["HOME"] + debug = _env.getenv("DEBUG", "false") + + This ensures consistent environment variable handling across the codebase. + diff --git a/.sg/tests/__snapshots__/os-environ-usage-snapshot.yml b/.sg/tests/__snapshots__/os-environ-usage-snapshot.yml new file mode 100644 index 00000000000..df11f9d9cf4 --- /dev/null +++ b/.sg/tests/__snapshots__/os-environ-usage-snapshot.yml @@ -0,0 +1,247 @@ +id: os-environ-usage +snapshots: + from os import environ: + labels: + - source: from os import environ + style: primary + start: 0 + end: 22 + ? | + from os import environ + env_keys = environ.keys() + : labels: + - source: from os import environ + style: primary + start: 0 + end: 22 + ? | + from os import environ + value = environ.get("SHELL") + : labels: + - source: from os import environ + style: primary + start: 0 + end: 22 + ? | + from os import environ + value = environ["USER"] + : labels: + - source: from os import environ + style: primary + start: 0 + end: 22 + from os import environ as env_dict: + labels: + - source: from os import environ as env_dict + style: primary + start: 0 + end: 34 + ? | + from os import environ as env_dict + value = env_dict.get("PATH") + : labels: + - source: from os import environ as env_dict + style: primary + start: 0 + end: 34 + ? | + from os import environ as env_dict + value = env_dict["HOME"] + : labels: + - source: from os import environ as env_dict + style: primary + start: 0 + end: 34 + from os import environ, getenv: + labels: + - source: from os import environ, getenv + style: primary + start: 0 + end: 30 + from os import getenv: + labels: + - source: from os import getenv + style: primary + start: 0 + end: 21 + ? | + from os import getenv + value = getenv("CONFIG") + : labels: + - source: from os import getenv + style: primary + start: 0 + end: 21 + ? | + from os import getenv + value = getenv("MODE", "production") + : labels: + - source: from os import getenv + style: primary + start: 0 + end: 21 + from os import getenv as get_env_func: + labels: + - source: from os import getenv as get_env_func + style: primary + start: 0 + end: 37 + ? | + from os import getenv as get_env_func + value = get_env_func("LEVEL") + : labels: + - source: from os import getenv as get_env_func + style: primary + start: 0 + end: 37 + ? | + from os import getenv as get_env_func + value = get_env_func("PORT", "8080") + : labels: + - source: from os import getenv as get_env_func + style: primary + start: 0 + end: 37 + from os import getenv, environ: + labels: + - source: from os import getenv, environ + style: primary + start: 0 + end: 30 + from os import path, environ, getenv: + labels: + - source: from os import path, environ, getenv + style: primary + start: 0 + end: 36 + ? | + import os + all_items = os.environ.items() + : labels: + - source: os.environ.items() + style: primary + start: 22 + end: 40 + ? | + import os + all_keys = os.environ.keys() + : labels: + - source: os.environ.keys() + style: primary + start: 21 + end: 38 + ? | + import os + all_values = os.environ.values() + : labels: + - source: os.environ.values() + style: primary + start: 23 + end: 42 + ? | + import os + default_val = os.environ.setdefault("DEF", "default") + : labels: + - source: os.environ.setdefault("DEF", "default") + style: primary + start: 24 + end: 63 + ? | + import os + env_copy = os.environ.copy() + : labels: + - source: os.environ.copy() + style: primary + start: 21 + end: 38 + ? | + import os + env_ref = os.environ + : labels: + - source: os.environ + style: primary + start: 20 + end: 30 + ? | + import os + for k, v in os.environ.items(): + pass + : labels: + - source: os.environ.items() + style: primary + start: 22 + end: 40 + ? | + import os + for key in os.environ: + pass + : labels: + - source: os.environ + style: primary + start: 21 + end: 31 + ? | + import os + if "HOME" in os.environ: + pass + : labels: + - source: os.environ + style: primary + start: 23 + end: 33 + ? | + import os + os.environ.clear() + : labels: + - source: os.environ.clear() + style: primary + start: 10 + end: 28 + ? | + import os + os.environ.update({"NEW_VAR": "value"}) + : labels: + - source: 'os.environ.update({"NEW_VAR": "value"})' + style: primary + start: 10 + end: 49 + ? | + import os + removed = os.environ.pop("TEMP_VAR", None) + : labels: + - source: os.environ.pop("TEMP_VAR", None) + style: primary + start: 20 + end: 52 + ? | + import os + value = os.environ.get("PATH") + : labels: + - source: os.environ.get("PATH") + style: primary + start: 18 + end: 40 + ? | + import os + value = os.environ["HOME"] + : labels: + - source: os.environ["HOME"] + style: primary + start: 18 + end: 36 + ? | + import os + value = os.getenv("DEBUG") + : labels: + - source: os.getenv("DEBUG") + style: primary + start: 18 + end: 36 + ? | + import os + value = os.getenv("TIMEOUT", "30") + : labels: + - source: os.getenv("TIMEOUT", "30") + style: primary + start: 18 + end: 44 diff --git a/.sg/tests/os-environ-usage-test.yml b/.sg/tests/os-environ-usage-test.yml new file mode 100644 index 00000000000..85f2f42bb53 --- /dev/null +++ b/.sg/tests/os-environ-usage-test.yml @@ -0,0 +1,129 @@ +id: os-environ-usage +valid: + # These should NOT trigger the rule (valid code) + - | + from ddtrace.setting import _env + debug = _env.getenv("DEBUG", "false") + value = _env.environ["HOME"] + - | + from ddtrace.settings._env import environ + value = environ["HOME"] + - | + from ddtrace.settings._env import getenv + debug = getenv("DEBUG", "false") + - | + from ddtrace.settings._env import environ, getenv + value = environ["HOME"] + debug = getenv("DEBUG", "false") + - | + # Using the centralized helpers is OK + from ddtrace.settings._env import environ + if "HOME" in environ: + print(environ["HOME"]) + +invalid: + # These should trigger the rule (errors) + + # Direct os.environ access patterns + - | + import os + value = os.environ["HOME"] + - | + import os + value = os.environ.get("PATH") + - | + import os + env_copy = os.environ.copy() + - | + import os + os.environ.clear() + - | + import os + os.environ.update({"NEW_VAR": "value"}) + - | + import os + removed = os.environ.pop("TEMP_VAR", None) + - | + import os + default_val = os.environ.setdefault("DEF", "default") + - | + import os + all_keys = os.environ.keys() + - | + import os + all_values = os.environ.values() + - | + import os + all_items = os.environ.items() + + # Membership tests and iteration + - | + import os + if "HOME" in os.environ: + pass + - | + import os + for key in os.environ: + pass + - | + import os + for k, v in os.environ.items(): + pass + + # os.getenv function calls + - | + import os + value = os.getenv("DEBUG") + - | + import os + value = os.getenv("TIMEOUT", "30") + + # Direct environ usage (imported from os) + - from os import environ + - | + from os import environ + value = environ["USER"] + - | + from os import environ + value = environ.get("SHELL") + - | + from os import environ + env_keys = environ.keys() + + # Aliased environ usage + - from os import environ as env_dict + - | + from os import environ as env_dict + value = env_dict["HOME"] + - | + from os import environ as env_dict + value = env_dict.get("PATH") + + # Direct getenv calls (imported from os) + - from os import getenv + - | + from os import getenv + value = getenv("CONFIG") + - | + from os import getenv + value = getenv("MODE", "production") + + # Aliased getenv calls + - from os import getenv as get_env_func + - | + from os import getenv as get_env_func + value = get_env_func("LEVEL") + - | + from os import getenv as get_env_func + value = get_env_func("PORT", "8080") + + # Mixed imports + - from os import environ, getenv + - from os import getenv, environ + - from os import path, environ, getenv + + # Direct attribute access + - | + import os + env_ref = os.environ +