DataDog / datadog-static-analyzer

Datadog Static Analyzer
https://docs.datadoghq.com/static_analysis/
Apache License 2.0
100 stars 12 forks source link

[STAL-2369] feat: add `javascript.ImportsPackage` stub #440

Closed amaanq closed 2 months ago

amaanq commented 3 months ago

What problem are you trying to solve?

Currently, we do not have a way to easily check if a JavaScript file imports a certain package. This is problematic because we cannot write rules that only target files that import React. It is also difficult to implement in individual rules as JavaScript has many different ways of importing modules and/or items, so it'd be really helpful if we had some way to just check if some module is imported without relying on our users to write complicated queries to check for every possible import style.

What is your solution?

Add a stub function javascript.importsPackage that allows the user to check if a package is imported or not. While the groundwork has been laid to allow users to get really powerful information about what packages are imported and be creative about how they use it, we do not expose all of this functionality yet as the ddsa.javascript API has not been finalized. As such, for now, we only expose a simple stub function to check for a package being imported or not, with the nitty-gritty details like what imports a file contains and if they're aliases or modules or not being unexposed in the API. The relevant classes and methods that contain this logic are marked as private.

The logic is quite similar to the already-implemented Go and Terraform FileContexts.

Tests cover the various ways a user may import JavaScript packages, since we have to account for ES6 modules, CommonJS requires, and dynamic imports, along with the various ways we can destructure these imports.

Alternatives considered

What the reviewer should know

The PackageImport data structure represents a JavaScript import. An import in JavaScript can either bring in the whole module or just a subset of the module, which are available directly in the file instead of having to be prefixed with the module name to be accessed. To elaborate on what I mean here, consider the following two imports:

import 'foo'; // brings the foo module into the file
// ...
foo.method();
console.log(foo.variable);

vs.

import { method, variable } from 'foo'; // brings in a subset of the module foo and brings them in directly
// ...
method();
console.log(variable);

In the first example, we consider foo to be the imported "name", with no alias or no module it's being imported from. However, the second example brings in method and variable as the imported "name", and is imported from foo.

The logic for figuring out whether a package is imported or not is as follows:

If we import a module directly, that is the module we check against, i.e., import.name. Otherwise, if we bring in a subset, we consider where it is imported from to be the module we check against, since we are bringing items from that module into scope, albeit a subset of it.

Hence, the logic for detecting if a module is imported or not looks like the following, in pseudocode:

function importsPackage(packageName) {
  for (const _import of this.imports) {
    if (_import.isAModule) {
      return _import.name === packageName;
    } else {
      return _import.importedFrom === packageName;
    }
}
amaanq commented 2 months ago

squashing & merging