Skip to content

Suggestion: require-namespace-imports #1547

Open
@OliverJAsh

Description

@OliverJAsh

For a given subset of modules, we would like to ensure that all imports for these modules use namespaces (aka not default or named imports).

Rationale

Inside of a module, sometimes we like to name things contextually according to the name of the module. This means that when these values are imported, they may appear ambiguous if they are not prefixed with a namespace. For example:

// ./query-params.ts

/** Get a query param */
export const get = (param: string) => { /* … */ }
// ./main.ts

import { get } from './query-params';

// 100 lines or so later…
// when skimming the code, it's not clear what `get` means?!
const result = get('foo');

Furthermore, we may need to import from other modules with named exports under the same name, e.g. in this example we may also need to import get from lodash.

// ./main.ts

// Naming conflicts
import { get } from './query-params';
import { get } from 'lodash-es';

(We see these naming conflicts happening more frequently now that many libraries are moving from instance methods to pipeable operators (e.g. RxJS, fp-ts, idb-keyval, Fluture, date-fns) because they have better bundling performance.)

If we use namespace imports, the names are no longer ambiguous and we no longer have naming conflicts:

// ./main.ts

import * as queryParams from './query-params';
import * as _ from 'lodash-es';

const result = queryParams.get('foo');

const result2 = _.get(/* … */);

Proof of concept

Here's how we're currently doing this:

// .eslintplugin.js

// This file allows us to define our own ESLint rules. It is powered by
// https://github.com/taskworld/eslint-plugin-local.

// Possible improvements:
// - rather than saying "has some child that is `ImportSpecifier` or `ImportDefaultSpecifier`", it
//   would be more explicit to say "has some specifier that is not `ImportNamespaceSpecifier`", but
//   I couldn't find a way to do this.
// - select whole import when match is found, rather than every child/specifier.
// - support for relative paths
/** @returns {import('eslint').Rule.RuleModule} */
const requireNamespaceImports = (() => {
  /** @param importPaths {string[]} */
  const createSelector = importPaths =>
    `:matches(${importPaths
      .map(importPath => `ImportDeclaration[source.value='${importPath}']`)
      .join(', ')}) :matches(ImportSpecifier, ImportDefaultSpecifier)`;

  return {
    create: context => {
      const [importPaths] = context.options;
      const selector = createSelector(importPaths);
      return {
        [selector]: node => {
          context.report({
            node,
            message: 'Only namespace imports are allowed for this module.',
          });
        },
      };
    },
  };
})();

exports.rules = {
  'require-namespace-imports': requireNamespaceImports,
};
// .eslintrc.js
module.exports = {
  plugins: ['local'],
  rules: {
    'local/require-namespace-imports': [
      2,
      [
        'shared/facades/option',
        'shared/facades/reactive-extensions',
        'shared/helpers/urls/search',
        'shared/helpers/urls/query',
      ],
    ]
  }
};

Questions

  • Should it be possible to specify the name of the namespace to be used for each import?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions