1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[PM-16231] Improved SDK referencing (#12475)

* feat: implement Rc

* feat: use Rc in sdk service

* docs: add an example to `take()`

* fix: clarify function doc

* Add custom eslint rule package with enforced `using` rule (#13009)

* feat: add custom eslint rule

* feat: check for `UsingRequired` instead of hardcoding `Rc`

* chore: move package to libs

* wip: add tests. Tests work when run from same folder but not from root

* fix: add dependencies to renovate

* fix: add empty ts file to avoid typechecking throwing errors

* fix: tests not running from root

* chore: remove unecessary config

* fix: linting

* docs: add readme

* chore: add platform ownership

* chore: clean up comment

* Add support for flat config to "Improved sdk referencing" (#13054)

* WIP flat config for eslint

* Add rxjs

* Configure vscode to use flat config

* Fix some new linting errors

* Remove directory overrides of .eslintrc

* Remove explicit dependencies on typescript-eslint/ and @angular-eslint/

* Add missing rules

* Add rxjs recommended rules

* Add storybook and enabled rxjs-angular rule

* Add buildNoRestrictedImports helper

* Ignore platform import restrictions

* Remove unused ignores

* feat: migrate rules over to .mjs and flat config

* feat: implement support for .mjs tests

* chore: remove old package approach

* chore: update package-lock

* fix: add empty TS file to stop errors

* chore: clean up comments

---------

Co-authored-by: Hinton <hinton@users.noreply.github.com>

* fix: update CODEOWNERS to match folder name

* fix: renovate.json after merge

* fix: package.json, pin versions, sort order

* fix: update package-lock.json

---------

Co-authored-by: Hinton <hinton@users.noreply.github.com>
This commit is contained in:
Andreas Coroiu
2025-02-03 15:09:25 +01:00
committed by GitHub
parent a0e48781bd
commit d7c46bb3a5
20 changed files with 3187 additions and 6138 deletions

View File

@@ -0,0 +1,3 @@
import requiredUsing from "./required-using.mjs";
export default { rules: { "required-using": requiredUsing } };

View File

@@ -0,0 +1,83 @@
import { ESLintUtils } from "@typescript-eslint/utils";
export const errorMessage = "'using' keyword is required but not used";
export default {
meta: {
type: "problem",
docs: {
description: "Ensure objects implementing UsingRequired are used with the using keyword",
category: "Best Practices",
recommended: false,
},
schema: [],
},
create(context) {
const parserServices = ESLintUtils.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
// Function to check if a type implements the `UsingRequired` interface
function implementsUsingRequired(type) {
const symbol = type.getSymbol();
if (!symbol) {
return false;
}
const declarations = symbol.getDeclarations() || [];
for (const declaration of declarations) {
const heritageClauses = declaration.heritageClauses || [];
for (const clause of heritageClauses) {
if (
clause.types.some(
(typeExpression) =>
checker.typeToString(checker.getTypeAtLocation(typeExpression.expression)) ===
"UsingRequired",
)
) {
return true;
}
}
}
return false;
}
// Function to check if a function call returns a `UsingRequired`
function returnsUsingRequired(node) {
if (node.type === "CallExpression") {
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const returnType = checker.getTypeAtLocation(tsNode);
return implementsUsingRequired(returnType);
}
return false;
}
return {
VariableDeclarator(node) {
// Skip if `using` is already present
if (node.parent.type === "VariableDeclaration" && node.parent.kind === "using") {
return;
}
// Check if the initializer returns a `UsingRequired`
if (node.init && returnsUsingRequired(node.init)) {
context.report({
node,
message: errorMessage,
});
}
},
AssignmentExpression(node) {
// Check if the right-hand side returns a `UsingRequired`
if (returnsUsingRequired(node.right)) {
context.report({
node,
message: errorMessage,
});
}
},
};
},
};

View File

@@ -0,0 +1,98 @@
import { RuleTester } from "@typescript-eslint/rule-tester";
import rule, { errorMessage } from "./required-using.mjs";
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
project: [__dirname + "/../tsconfig.spec.json"],
projectService: {
allowDefaultProject: ["*.ts*"],
},
tsconfigRootDir: __dirname + "/..",
},
},
});
const setup = `
interface UsingRequired {}
class Ref implements UsingRequired {}
const rc = {
take(): Ref {
return new Ref();
},
};
`;
ruleTester.run("required-using", rule.default, {
valid: [
{
name: "Direct declaration with `using`",
code: `
${setup}
using client = rc.take();
`,
},
{
name: "Function reference with `using`",
code: `
${setup}
const t = rc.take;
using client = t();
`,
},
],
invalid: [
{
name: "Direct declaration without `using`",
code: `
${setup}
const client = rc.take();
`,
errors: [
{
message: errorMessage,
},
],
},
{
name: "Assignment without `using`",
code: `
${setup}
let client;
client = rc.take();
`,
errors: [
{
message: errorMessage,
},
],
},
{
name: "Function reference without `using`",
code: `
${setup}
const t = rc.take;
const client = t();
`,
errors: [
{
message: errorMessage,
},
],
},
{
name: "Destructuring without `using`",
code: `
${setup}
const { value } = rc.take();
`,
errors: [
{
message: errorMessage,
},
],
},
],
});