Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make service containers readonly #1635

Merged
merged 2 commits into from
Aug 29, 2024
Merged

Make service containers readonly #1635

merged 2 commits into from
Aug 29, 2024

Conversation

msujew
Copy link
Member

@msujew msujew commented Aug 17, 2024

Fixes something that was encountered in #1621 (reply in thread).

Essentially, our API (and runtime as well - to some degree) allows to override the values of services simply by performing assignments. However, this almost certainly breaks runtime behavior in case the service that is being assigned is used as a service dependency somewhere.

This change adds readonly modifiers where necessary and adds a set method to the service proxies that ensure that no service can be assigned.

@msujew msujew added the polish Some feature needs improvement label Aug 17, 2024
@msujew msujew added this to the v3.2.0 milestone Aug 22, 2024
Copy link
Contributor

@dhuebner dhuebner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not clear to me that it was possible. Or I just never had an idea to do this...
Even thought it's a bad idea to override the services, it is as breaking change because of changing public API. We need a dedicated entry in the change log.

Copy link
Contributor

@Lotes Lotes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small findings/understanding questions :)...

const obj: any = Object.seal(inject({}));
expect(Object.isExtensible(obj)).toBe(false);
expect(() => (obj.a = 1)).toThrowError('Cannot define property a, object is not extensible');
expect(() => (obj.a = 1)).toThrowError('Cannot set property on injected service container');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks a bit weird because a was not initially set to a value.
I would assume that any property that is not defined on this object is inaccessiable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the set override on the proxy it was previously completely valid to set the value. This test ensures that it's not possible.

getOwnPropertyDescriptor: (obj, prop) => (_resolve(obj, prop, module, injector || proxy), Object.getOwnPropertyDescriptor(obj, prop)), // used by for..in
has: (_, prop) => prop in module, // used by ..in..
ownKeys: () => [...Reflect.ownKeys(module), isProxy] // used by for..in
ownKeys: () => [...Object.getOwnPropertyNames(module)] // used by for..in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference here? The documentation says it is the same, except that symbols are not listed with Object.getOwnPropertyNames.
What is the idea behind this change? On first sight it looks more complicated, because you need to handle isProxy more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isProxy is a very special variable that should not be exposed from the service container using reflection. We only need it internally.

@@ -28,7 +28,7 @@ import { DefaultWorkspaceSymbolProvider } from './workspace-symbol-provider.js';
* Context required for creating the default language-specific dependency injection module.
*/
export interface DefaultModuleContext extends DefaultCoreModuleContext {
shared: LangiumSharedServices;
readonly shared: LangiumSharedServices;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are throwing an Error on the setter of the proxy, would it be a good idea to require that all properties of the Module are readonly?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's possible to enforce this on the type level in TypeScript.

@msujew msujew merged commit 5063a2b into main Aug 29, 2024
5 checks passed
@msujew msujew deleted the msujew/readonly-services branch August 29, 2024 12:06
@pbrostean
Copy link

pbrostean commented Nov 5, 2024

I just saw that my code is no longer working and saw that this change caused it. I used to create my own FoldingRangeProvicer and CommandHandler and overwrote lsp.FoldingRangeProvider and lsp.ExecuteCommandHandler with them. Is there any possibility of changing the behaviour without overriding them in this version?

@msujew
Copy link
Member Author

msujew commented Nov 5, 2024

@pbrostean You're supposed to use the Module API to change the implementation used for a specific service. Everything else will likely result in runtime errors (see the referenced discussion).

@pbrostean
Copy link

@msujew I've alreadly tried using the Module API but never managed to change the implementation of the FoldingRangeProvider this way. Would you mind sharing an example?

@cdietrich
Copy link
Contributor

simply

export const YourDslModule ....
lsp: {
 ....
 FoldingRangeProvider: services => new YourFoldingRangeProvider(services),
}

does not work?
see LangiumGrammarModule in the codebase of this repo

@pbrostean
Copy link

It does, thank you! I don't know why I had it in the declaration of custom services. Is there an equal implementation for a custom ExecuteCommandHandler?

@msujew
Copy link
Member Author

msujew commented Nov 6, 2024

@pbrostean Yes, but the command handler needs to go into the shared services:

export const YourDslSharedModule = {
  lsp: {
    CommandHandler: services => new YourDslCommandHandler(services)
  }
}

See also here and here for some examples.

@pbrostean
Copy link

@msujew @cdietrich you two are the best, thank's for the super fast replies!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
polish Some feature needs improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants