├── .gitignore
├── .php_cs
├── .travis.yml
├── CHANGELOG.md
├── COPYING
├── README.md
├── bin
└── tenkawa.php
├── composer.json
├── composer.lock
├── images
└── tenkawa-logo.png
├── phpstan.neon
├── phpunit.xml.dist
├── src
└── Tsufeki
│ └── Tenkawa
│ ├── BeberleiAssert
│ └── BeberleiAssertPlugin.php
│ ├── Doctrine
│ └── DoctrinePlugin.php
│ ├── Mockery
│ └── MockeryPlugin.php
│ ├── Phony
│ └── PhonyPlugin.php
│ ├── Php
│ ├── Composer
│ │ ├── ComposerFileFilter.php
│ │ └── ComposerService.php
│ ├── Feature
│ │ ├── Completion
│ │ │ ├── GlobalSymbolCompleter.php
│ │ │ ├── ImportSymbolCompleter.php
│ │ │ ├── MemberSymbolCompleter.php
│ │ │ ├── SymbolCompleter.php
│ │ │ ├── SymbolCompletionProvider.php
│ │ │ ├── VariableCompletionProvider.php
│ │ │ ├── VariableGatheringVisitor.php
│ │ │ └── WholeFileSnippetCompletionProvider.php
│ │ ├── DocumentSymbols
│ │ │ └── SymbolDocumentSymbolsProvider.php
│ │ ├── GoToDefinition
│ │ │ ├── ParentMemberGoToDefinitionProvider.php
│ │ │ └── SymbolGoToDefinitionProvider.php
│ │ ├── GoToImplementation
│ │ │ ├── ClassLikeGoToImplementationProvider.php
│ │ │ ├── FindClassLikeImplementationsVisitor.php
│ │ │ ├── FindMemberImplementationsVisitor.php
│ │ │ └── MemberGoToImplementationProvider.php
│ │ ├── Hover
│ │ │ ├── ExpressionTypeHoverProvider.php
│ │ │ ├── HoverFormatter.php
│ │ │ └── SymbolHoverProvider.php
│ │ ├── Refactoring
│ │ │ ├── Differ.php
│ │ │ ├── EditHelper.php
│ │ │ ├── FineDiffer.php
│ │ │ ├── FixAutoloadClassNameRefactoring.php
│ │ │ ├── ImportCodeActionProvider.php
│ │ │ ├── ImportEditData.php
│ │ │ ├── Importer.php
│ │ │ ├── Indent.php
│ │ │ ├── RefactoringExecutor.php
│ │ │ └── WorkspaceEditCommandProvider.php
│ │ ├── References
│ │ │ ├── FindInheritedMembersVisitor.php
│ │ │ ├── GlobalReferenceFinder.php
│ │ │ ├── GlobalReferencesIndexDataProvider.php
│ │ │ ├── MemberReferenceFinder.php
│ │ │ ├── Reference.php
│ │ │ ├── ReferenceFinder.php
│ │ │ └── SymbolReferencesProvider.php
│ │ ├── SignatureHelp
│ │ │ ├── ReflectionSignatureFinder.php
│ │ │ ├── SignatureFinder.php
│ │ │ └── SymbolSignatureHelpProvider.php
│ │ └── WorkspaceSymbols
│ │ │ └── ReflectionWorkspaceSymbolsProvider.php
│ ├── Index
│ │ └── StubsIndexer.php
│ ├── NodeFinder
│ │ ├── NameContextTaggingVisitor.php
│ │ └── NodeFinder.php
│ ├── Parser
│ │ ├── Ast.php
│ │ ├── FindIntersectingNodesVisitor.php
│ │ ├── FindNodeVisitor.php
│ │ ├── Parser.php
│ │ ├── ParserDiagnosticsProvider.php
│ │ ├── PhpParserAdapter.php
│ │ └── TokenIterator.php
│ ├── PhpDoc
│ │ └── PhpDocFormatter.php
│ ├── PhpPlugin.php
│ ├── PhpStan
│ │ ├── Analyser
│ │ │ ├── AnalysedCacheAware.php
│ │ │ ├── AnalysedDocumentAware.php
│ │ │ ├── AnalysedProjectAware.php
│ │ │ └── Analyser.php
│ │ ├── IndexReflection
│ │ │ ├── DummyFunctionReflectionFactory.php
│ │ │ ├── DummyReflectionClass.php
│ │ │ ├── DummyReflectionMethod.php
│ │ │ ├── DummyReflectionProperty.php
│ │ │ ├── DummyReflectionType.php
│ │ │ ├── IndexBroker.php
│ │ │ ├── IndexClassConstantReflection.php
│ │ │ ├── IndexClassReflection.php
│ │ │ ├── IndexFunctionReflection.php
│ │ │ ├── IndexMethodReflection.php
│ │ │ ├── IndexParameterReflection.php
│ │ │ ├── IndexPropertyReflection.php
│ │ │ └── SignatureVariantFactory.php
│ │ ├── PhpDocResolver
│ │ │ ├── PhpDocResolver.php
│ │ │ └── PhpDocResolverVisitor.php
│ │ ├── PhpStanDiagnosticsProvider.php
│ │ ├── PhpStanSignatureFinder.php
│ │ ├── PhpStanTypeInference.php
│ │ └── Utils
│ │ │ ├── AstPruner.php
│ │ │ ├── DocumentParser.php
│ │ │ ├── ErrorTolerantPrettyPrinter.php
│ │ │ └── patch.php
│ ├── Reflection
│ │ ├── ClassResolver.php
│ │ ├── ClassResolverExtension.php
│ │ ├── ConstExprEvaluation.php
│ │ ├── ConstExprEvaluator.php
│ │ ├── Element
│ │ │ ├── ClassConst.php
│ │ │ ├── ClassLike.php
│ │ │ ├── Const_.php
│ │ │ ├── DocComment.php
│ │ │ ├── Element.php
│ │ │ ├── Function_.php
│ │ │ ├── MemberTrait.php
│ │ │ ├── Method.php
│ │ │ ├── Param.php
│ │ │ ├── Property.php
│ │ │ ├── TraitAlias.php
│ │ │ ├── TraitInsteadOf.php
│ │ │ ├── Type.php
│ │ │ └── Variable.php
│ │ ├── IndexReflectionProvider.php
│ │ ├── InheritPhpDocClassResolverExtension.php
│ │ ├── InheritanceTreeTraverser.php
│ │ ├── InheritanceTreeVisitor.php
│ │ ├── MembersFromAnnotationClassResolverExtension.php
│ │ ├── NameContext.php
│ │ ├── NameContextVisitor.php
│ │ ├── NameHelper.php
│ │ ├── ReflectionIndexDataProvider.php
│ │ ├── ReflectionProvider.php
│ │ ├── ReflectionTransformer.php
│ │ ├── ReflectionVisitor.php
│ │ ├── Resolved
│ │ │ ├── ResolvedClassConst.php
│ │ │ ├── ResolvedClassLike.php
│ │ │ ├── ResolvedMemberTrait.php
│ │ │ ├── ResolvedMethod.php
│ │ │ └── ResolvedProperty.php
│ │ └── StubsReflectionTransformer.php
│ ├── Symbol
│ │ ├── DefinitionSymbol.php
│ │ ├── DefinitionSymbolExtractor.php
│ │ ├── DocCommentSymbolExtractor.php
│ │ ├── GlobalSymbol.php
│ │ ├── GlobalSymbolExtractor.php
│ │ ├── MemberSymbol.php
│ │ ├── MemberSymbolExtractor.php
│ │ ├── NodePathSymbolExtractor.php
│ │ ├── Symbol.php
│ │ ├── SymbolExtractor.php
│ │ └── SymbolReflection.php
│ └── TypeInference
│ │ ├── BasicType.php
│ │ ├── IntersectionType.php
│ │ ├── ObjectType.php
│ │ ├── Type.php
│ │ ├── TypeInference.php
│ │ └── UnionType.php
│ ├── PhpUnit
│ ├── MockBuilderDynamicReturnTypeExtension.php
│ └── PhpUnitPlugin.php
│ ├── Prophecy
│ └── ProphecyPlugin.php
│ ├── Server
│ ├── Document
│ │ ├── Document.php
│ │ ├── DocumentStore.php
│ │ └── Project.php
│ ├── Event
│ │ ├── Document
│ │ │ ├── OnChange.php
│ │ │ ├── OnClose.php
│ │ │ ├── OnOpen.php
│ │ │ ├── OnProjectClose.php
│ │ │ └── OnProjectOpen.php
│ │ ├── EventDispatcher.php
│ │ ├── OnFileChange.php
│ │ ├── OnIndexingFinished.php
│ │ ├── OnInit.php
│ │ ├── OnShutdown.php
│ │ └── OnStart.php
│ ├── Exception
│ │ ├── CancelledException.php
│ │ ├── DocumentNotOpenException.php
│ │ ├── IoException.php
│ │ ├── ProjectNotOpenException.php
│ │ ├── RequestCancelledException.php
│ │ ├── TransportException.php
│ │ ├── UnknownCommandException.php
│ │ └── UriException.php
│ ├── Feature
│ │ ├── Capabilities
│ │ │ ├── ClientCapabilities.php
│ │ │ ├── CompletionOptions.php
│ │ │ ├── DocumentSymbolClientCapabilities.php
│ │ │ ├── DynamicRegistrationCapability.php
│ │ │ ├── ExecuteCommandOptions.php
│ │ │ ├── GoToClientCapabilities.php
│ │ │ ├── InitializeResult.php
│ │ │ ├── SaveOptions.php
│ │ │ ├── ServerCapabilities.php
│ │ │ ├── SignatureHelpOptions.php
│ │ │ ├── TextDocumentClientCapabilities.php
│ │ │ ├── TextDocumentSyncKind.php
│ │ │ ├── TextDocumentSyncOptions.php
│ │ │ ├── WorkspaceClientCapabilities.php
│ │ │ ├── WorkspaceEditClientCapabilities.php
│ │ │ ├── WorkspaceFoldersServerCapabilities.php
│ │ │ └── WorkspaceServerCapabilities.php
│ │ ├── CodeAction
│ │ │ ├── CodeActionContext.php
│ │ │ ├── CodeActionFeature.php
│ │ │ └── CodeActionProvider.php
│ │ ├── Command
│ │ │ ├── CommandFeature.php
│ │ │ └── CommandProvider.php
│ │ ├── Common
│ │ │ ├── Command.php
│ │ │ ├── Location.php
│ │ │ ├── LocationLink.php
│ │ │ ├── MarkupContent.php
│ │ │ ├── MarkupKind.php
│ │ │ ├── Position.php
│ │ │ ├── Range.php
│ │ │ ├── SymbolInformation.php
│ │ │ ├── SymbolKind.php
│ │ │ ├── TextDocumentEdit.php
│ │ │ ├── TextDocumentIdentifier.php
│ │ │ ├── TextDocumentItem.php
│ │ │ ├── TextEdit.php
│ │ │ ├── VersionedTextDocumentIdentifier.php
│ │ │ └── WorkspaceEdit.php
│ │ ├── Completion
│ │ │ ├── CompletionContext.php
│ │ │ ├── CompletionFeature.php
│ │ │ ├── CompletionItem.php
│ │ │ ├── CompletionItemKind.php
│ │ │ ├── CompletionList.php
│ │ │ ├── CompletionProvider.php
│ │ │ ├── CompletionTriggerKind.php
│ │ │ └── InsertTextFormat.php
│ │ ├── Configuration
│ │ │ ├── ConfigurationFeature.php
│ │ │ └── ConfigurationItem.php
│ │ ├── Diagnostics
│ │ │ ├── Diagnostic.php
│ │ │ ├── DiagnosticSeverity.php
│ │ │ ├── DiagnosticsFeature.php
│ │ │ ├── DiagnosticsProvider.php
│ │ │ └── WorkspaceDiagnosticsProvider.php
│ │ ├── DocumentSymbols
│ │ │ ├── DocumentSymbol.php
│ │ │ ├── DocumentSymbolsFeature.php
│ │ │ └── DocumentSymbolsProvider.php
│ │ ├── Feature.php
│ │ ├── FileWatcher
│ │ │ ├── DidChangeWatchedFilesRegistrationOptions.php
│ │ │ ├── FileChangeType.php
│ │ │ ├── FileEvent.php
│ │ │ ├── FileSystemWatcher.php
│ │ │ ├── FileWatcherFeature.php
│ │ │ └── WatchKind.php
│ │ ├── GoToDefinition
│ │ │ ├── GoToDefinitionFeature.php
│ │ │ └── GoToDefinitionProvider.php
│ │ ├── GoToImplementation
│ │ │ ├── GoToImplementationFeature.php
│ │ │ └── GoToImplementationProvider.php
│ │ ├── Hover
│ │ │ ├── Hover.php
│ │ │ ├── HoverFeature.php
│ │ │ ├── HoverProvider.php
│ │ │ └── MarkedString.php
│ │ ├── LanguageServer.php
│ │ ├── Message
│ │ │ ├── MessageActionItem.php
│ │ │ ├── MessageFeature.php
│ │ │ └── MessageType.php
│ │ ├── ProgressNotification
│ │ │ ├── Progress.php
│ │ │ ├── ProgressGroup.php
│ │ │ └── ProgressNotificationFeature.php
│ │ ├── References
│ │ │ ├── ReferenceContext.php
│ │ │ ├── ReferencesFeature.php
│ │ │ └── ReferencesProvider.php
│ │ ├── Registration
│ │ │ ├── Registration.php
│ │ │ ├── RegistrationFeature.php
│ │ │ └── Unregistration.php
│ │ ├── SignatureHelp
│ │ │ ├── ParameterInformation.php
│ │ │ ├── SignatureHelp.php
│ │ │ ├── SignatureHelpFeature.php
│ │ │ ├── SignatureHelpProvider.php
│ │ │ └── SignatureInformation.php
│ │ ├── TextDocument
│ │ │ ├── TextDocumentContentChangeEvent.php
│ │ │ └── TextDocumentFeature.php
│ │ ├── Workspace
│ │ │ ├── WorkspaceFeature.php
│ │ │ ├── WorkspaceFolder.php
│ │ │ └── WorkspaceFoldersChangeEvent.php
│ │ ├── WorkspaceEdit
│ │ │ ├── ApplyWorkspaceEditResponse.php
│ │ │ └── WorkspaceEditFeature.php
│ │ └── WorkspaceSymbols
│ │ │ ├── WorkspaceSymbolsFeature.php
│ │ │ └── WorkspaceSymbolsProvider.php
│ ├── Index
│ │ ├── FileFilterFactory.php
│ │ ├── FileWatcherHandler.php
│ │ ├── GlobalIndexer.php
│ │ ├── Index.php
│ │ ├── IndexDataProvider.php
│ │ ├── IndexEntry.php
│ │ ├── IndexStorageFactory.php
│ │ ├── Indexer.php
│ │ ├── LocalCacheIndexStorageFactory.php
│ │ ├── MemoryIndexStorageFactory.php
│ │ ├── Query.php
│ │ └── Storage
│ │ │ ├── ChainedStorage.php
│ │ │ ├── IndexStorage.php
│ │ │ ├── MemoryStorage.php
│ │ │ ├── MergedStorage.php
│ │ │ ├── OpenDocumentsStorage.php
│ │ │ ├── SqliteStorage.php
│ │ │ └── WritableIndexStorage.php
│ ├── Io
│ │ ├── Directories.php
│ │ ├── FileLister
│ │ │ ├── FileFilter.php
│ │ │ ├── FileLister.php
│ │ │ ├── GlobFileFilter.php
│ │ │ ├── GlobRejectDirectoryFilter.php
│ │ │ └── LocalFileLister.php
│ │ ├── FileReader.php
│ │ ├── FileWatcher
│ │ │ ├── ClosedDocumentFileWatcher.php
│ │ │ ├── FileChangeDeduplicator.php
│ │ │ ├── FileWatcher.php
│ │ │ └── InotifyWaitFileWatcher.php
│ │ └── LocalFileReader.php
│ ├── Logger
│ │ ├── ClientLogger.php
│ │ ├── CompositeLogger.php
│ │ ├── LevelFilteringLogger.php
│ │ ├── LoggerTrait.php
│ │ └── StreamLogger.php
│ ├── Mapper
│ │ ├── PrefixStrippingUriMapper.php
│ │ └── UriMapper.php
│ ├── Plugin.php
│ ├── PluginFinder.php
│ ├── ServerPlugin.php
│ ├── ServerPluginInit.php
│ ├── Tenkawa.php
│ ├── Transport
│ │ ├── RunnableTransport.php
│ │ └── StreamTransport.php
│ ├── Uri.php
│ └── Utils
│ │ ├── Cache.php
│ │ ├── Event.php
│ │ ├── FuzzyMatcher.php
│ │ ├── InfiniteRecursionMarker.php
│ │ ├── KeyValueStateTrait.php
│ │ ├── NestedKernelsSyncAsync.php
│ │ ├── Platform.php
│ │ ├── PositionUtils.php
│ │ ├── PriorityKernel
│ │ ├── Priority.php
│ │ ├── PriorityStrand.php
│ │ ├── ReactScheduler.php
│ │ ├── ScheduledApi.php
│ │ ├── ScheduledReactKernel.php
│ │ └── Scheduler.php
│ │ ├── Stopwatch.php
│ │ ├── StringTemplate.php
│ │ ├── StringUtils.php
│ │ ├── SyncAsync.php
│ │ ├── SyncCallContext.php
│ │ ├── Template.php
│ │ └── Throttler.php
│ ├── Symfony
│ ├── Container
│ │ ├── ServiceMapUpdater.php
│ │ └── ServiceMapWatcher.php
│ └── SymfonyPlugin.php
│ └── WebMozartAssert
│ └── WebMozartAssertPlugin.php
└── tests
└── Tsufeki
└── Tenkawa
├── DummyTransportPair.php
├── FunctionalTest.php
├── Php
├── Parser
│ └── FindNodeVisitorTest.php
└── Reflection
│ └── ReflectionVisitorTest.php
├── Server
├── Document
│ └── DocumentStoreTest.php
├── Event
│ └── EventDispatcherTest.php
├── Feature
│ └── Diagnostics
│ │ └── DiagnosticsFeatureTest.php
├── Index
│ └── Storage
│ │ ├── ChainedStorageTest.php
│ │ ├── MemoryStorageTest.php
│ │ ├── MergedStorageTest.php
│ │ ├── SqliteStorageTest.php
│ │ ├── SqliteStorageWithPrefixTest.php
│ │ └── WritableIndexStorageTest.php
├── Transport
│ └── StreamTransportTest.php
├── UriTest.php
└── Utils
│ ├── EventTest.php
│ ├── PositionUtilsTest.php
│ ├── StringUtilsTest.php
│ └── SyncAsyncTest.php
├── TestCase.php
└── fixtures
└── Foo
└── SelfCompletion.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /data/
3 | /.php_cs.cache
4 |
--------------------------------------------------------------------------------
/.php_cs:
--------------------------------------------------------------------------------
1 | in(__DIR__);
5 |
6 | return PhpCsFixer\Config::create()
7 | ->setRules([
8 | '@Symfony' => true,
9 | 'align_multiline_comment' => [
10 | 'comment_type' => 'all_multiline',
11 | ],
12 | 'array_syntax' => [
13 | 'syntax' => 'short',
14 | ],
15 | 'blank_line_after_opening_tag' => false,
16 | 'blank_line_before_statement' => [
17 | 'statements' => ['return', 'throw', 'try'],
18 | ],
19 | 'braces' => [
20 | 'allow_single_line_closure' => true,
21 | ],
22 | 'cast_spaces' => [
23 | 'space' => 'none',
24 | ],
25 | 'concat_space' => [
26 | 'spacing' => 'one',
27 | ],
28 | 'declare_strict_types' => true,
29 | 'increment_style' => [
30 | 'style' => 'post',
31 | ],
32 | 'is_null' => [
33 | 'use_yoda_style' => false,
34 | ],
35 | 'modernize_types_casting' => true,
36 | 'no_alias_functions' => true,
37 | 'no_break_comment' => false,
38 | 'no_superfluous_elseif' => true,
39 | 'no_useless_else' => true,
40 | 'ordered_imports' => true,
41 | 'php_unit_construct' => true,
42 | 'php_unit_dedicate_assert' => true,
43 | 'php_unit_expectation' => true,
44 | 'php_unit_mock' => true,
45 | 'php_unit_namespaced' => true,
46 | 'php_unit_no_expectation_annotation' => true,
47 | 'php_unit_test_class_requires_covers' => true,
48 | 'phpdoc_annotation_without_dot' => false,
49 | 'phpdoc_summary' => false,
50 | 'phpdoc_to_comment' => false,
51 | 'pow_to_exponentiation' => true,
52 | 'random_api_migration' => true,
53 | 'space_after_semicolon' => [
54 | 'remove_in_empty_for_expressions' => true,
55 | ],
56 | 'ternary_to_null_coalescing' => true,
57 | 'yoda_style' => false,
58 | ])
59 | ->setFinder($finder);
60 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - '7.1'
5 | - '7.2'
6 | - '7.3'
7 | - '7.4'
8 |
9 | cache:
10 | directories:
11 | - $HOME/.composer/cache
12 |
13 | before_install:
14 | - phpenv config-rm xdebug.ini
15 |
16 | install:
17 | - composer install --no-interaction
18 |
19 | script:
20 | - ./vendor/bin/phpstan analyze --configuration=phpstan.neon --level=max --no-interaction --no-progress src/ tests/ bin/
21 | - ./vendor/bin/phpunit
22 |
--------------------------------------------------------------------------------
/bin/tenkawa.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | = 7.1\n");
9 | exit(1);
10 | }
11 |
12 | foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
13 | if (file_exists($file)) {
14 | require_once $file;
15 | break;
16 | }
17 | }
18 | unset($file);
19 |
20 | $xdebug = new XdebugHandler('tenkawa');
21 | $xdebug->check();
22 | unset($xdebug);
23 |
24 | $requiredExtensions = [
25 | 'pdo_sqlite' => 2,
26 | 'mbstring' => 3,
27 | ];
28 |
29 | foreach ($requiredExtensions as $ext => $errorCode) {
30 | if (!extension_loaded($ext)) {
31 | fprintf(STDERR, "Tenkawa requires $ext extension\n");
32 | exit($errorCode);
33 | }
34 | }
35 | unset($requiredExtensions, $ext, $errorCode);
36 |
37 | if (!class_exists(Tenkawa::class)) {
38 | fprintf(STDERR, "Tenkawa was not properly installed\n");
39 | exit(9);
40 | }
41 |
42 | Tenkawa::main($argv);
43 |
--------------------------------------------------------------------------------
/images/tenkawa-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsufeki/tenkawa-php-language-server/90ffe347a664a39af538ea943147081e9d341b45/images/tenkawa-logo.png
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - vendor/phpstan/phpstan-phpunit/extension.neon
3 | - vendor/phpstan/phpstan-phpunit/rules.neon
4 |
5 | parameters:
6 | excludes_analyse:
7 | - '%rootDir%/../../../tests/Tsufeki/Tenkawa/fixtures/*'
8 | ignoreErrors:
9 | - '#Call to an undefined static method Recoil\\Recoil::eventLoop\(\)#'
10 | - '#(Tsufeki\\Tenkawa\\Php\\PhpStan\\.*|Anonymous.*)::__construct\(\) does not call parent constructor from (PHPStan\\.*|Reflection(Class|Property|Method)|PhpParser\\PrettyPrinterAbstract|Tsufeki\\Tenkawa\\Php\\PhpStan\\Utils\\DocumentParser)#'
11 | - '#Call to an undefined method PDO::sqliteCreateFunction\(\)#'
12 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 | ./tests/
16 |
17 |
18 |
19 |
20 |
21 | ./src/
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/BeberleiAssert/BeberleiAssertPlugin.php:
--------------------------------------------------------------------------------
1 | setClass(DynamicFunctionReturnTypeExtension::class, AssertThatFunctionDynamicReturnTypeExtension::class, true);
23 | $container->setClass(DynamicMethodReturnTypeExtension::class, AssertionChainDynamicReturnTypeExtension::class, true);
24 | $container->setClass(DynamicStaticMethodReturnTypeExtension::class, AssertThatDynamicMethodReturnTypeExtension::class, true);
25 | $container->setClass(MethodTypeSpecifyingExtension::class, AssertionChainTypeSpecifyingExtension::class, true);
26 | $container->setClass(StaticMethodTypeSpecifyingExtension::class, AssertTypeSpecifyingExtension::class, true);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Doctrine/DoctrinePlugin.php:
--------------------------------------------------------------------------------
1 | setClass(DynamicMethodReturnTypeExtension::class, DoctrineSelectableDynamicReturnTypeExtension::class, true);
22 | $container->setClass(DynamicMethodReturnTypeExtension::class, EntityManagerFindDynamicReturnTypeExtension::class, true);
23 | $container->setClass(DynamicMethodReturnTypeExtension::class, EntityManagerGetRepositoryDynamicReturnTypeExtension::class, true, [new Value('Doctrine\ORM\EntityRepository')]);
24 | $container->setClass(DynamicMethodReturnTypeExtension::class, EntityRepositoryDynamicReturnTypeExtension::class, true);
25 | $container->setClass(DynamicMethodReturnTypeExtension::class, ObjectManagerMergeDynamicReturnTypeExtension::class, true);
26 | $container->setClass(MethodsClassReflectionExtension::class, DoctrineSelectableClassReflectionExtension::class, true);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Phony/PhonyPlugin.php:
--------------------------------------------------------------------------------
1 | setClass(DynamicMethodReturnTypeExtension::class, InstanceHandleGetReturnType::class, true);
23 | $container->setClass(DynamicMethodReturnTypeExtension::class, MockBuilderGetReturnType::class, true);
24 | $container->setClass(PropertiesClassReflectionExtension::class, HandleProperties::class, true);
25 |
26 | foreach ([
27 | 'Eloquent\\Phony',
28 | 'Eloquent\\Phony\\Kahlan',
29 | 'Eloquent\\Phony\\Phpunit',
30 | 'Eloquent\\Phony\\Pho',
31 | ] as $namespace) {
32 | $container->setClass(MockBuilderReturnType::class, null, false, [new Value($namespace)]);
33 | $container->setAlias(DynamicFunctionReturnTypeExtension::class, MockBuilderReturnType::class, true);
34 | $container->setAlias(DynamicStaticMethodReturnTypeExtension::class, MockBuilderReturnType::class, true);
35 |
36 | $container->setClass(MockReturnType::class, null, false, [new Value($namespace)]);
37 | $container->setAlias(DynamicFunctionReturnTypeExtension::class, MockReturnType::class, true);
38 | $container->setAlias(DynamicStaticMethodReturnTypeExtension::class, MockReturnType::class, true);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Composer/ComposerFileFilter.php:
--------------------------------------------------------------------------------
1 | rejectGlobs = $this->normalize($rejectGlobs);
34 | $this->acceptGlobs = $this->normalize($acceptGlobs);
35 | $this->forceRejectGlobs = $this->normalize($forceRejectGlobs);
36 | }
37 |
38 | private function normalize(array $globs): array
39 | {
40 | return array_map(function ($glob) {
41 | return Uri::fromString($glob)->getNormalizedGlob();
42 | }, array_unique($globs));
43 | }
44 |
45 | public function filter(string $uri, string $baseUri): int
46 | {
47 | $accept = !$this->matchArray($this->rejectGlobs, $uri)
48 | || ($this->matchArray($this->acceptGlobs, $uri)
49 | && !$this->matchArray($this->forceRejectGlobs, $uri));
50 |
51 | return $accept ? self::ABSTAIN : self::REJECT;
52 | }
53 |
54 | public function getFileType(): string
55 | {
56 | return '';
57 | }
58 |
59 | public function enterDirectory(string $uri, string $baseUri): int
60 | {
61 | return self::ABSTAIN;
62 | }
63 |
64 | private function matchArray(array $globs, string $uri): bool
65 | {
66 | foreach ($globs as $glob) {
67 | if (Glob::match($uri, $glob)) {
68 | return true;
69 | }
70 | }
71 |
72 | return false;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Feature/Completion/SymbolCompleter.php:
--------------------------------------------------------------------------------
1 | symbolExtractor = $symbolExtractor;
33 | $this->symbolCompleters = $symbolCompleters;
34 | }
35 |
36 | public function getTriggerCharacters(): array
37 | {
38 | return array_merge(...array_map(function (SymbolCompleter $completer) {
39 | return $completer->getTriggerCharacters();
40 | }, $this->symbolCompleters));
41 | }
42 |
43 | public function getCompletions(
44 | Document $document,
45 | Position $position,
46 | ?CompletionContext $context
47 | ): \Generator {
48 | if ($document->getLanguage() !== 'php') {
49 | return new CompletionList();
50 | }
51 |
52 | /** @var Symbol|null */
53 | $symbol = yield $this->symbolExtractor->getSymbolAt($document, $position, true);
54 | if ($symbol === null) {
55 | return new CompletionList();
56 | }
57 |
58 | return CompletionList::merge(yield array_map(function (SymbolCompleter $completer) use ($symbol, $position) {
59 | return $completer->getCompletions($symbol, $position);
60 | }, $this->symbolCompleters));
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Feature/Completion/VariableGatheringVisitor.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | private $variables;
19 |
20 | /**
21 | * @param array $initialVariables
22 | */
23 | public function __construct(array $initialVariables = [])
24 | {
25 | $this->variables = $initialVariables;
26 | }
27 |
28 | public function enterNode(Node $node)
29 | {
30 | if ($node instanceof Expr\Variable && is_string($node->name)) {
31 | $this->variables[$node->name] = $node->getAttribute('type');
32 |
33 | return null;
34 | }
35 |
36 | if ($node instanceof FunctionLike || $node instanceof Stmt\ClassLike) {
37 | return NodeTraverser::DONT_TRAVERSE_CHILDREN;
38 | }
39 |
40 | return null;
41 | }
42 |
43 | /**
44 | * @return array
45 | */
46 | public function getVariables(): array
47 | {
48 | return $this->variables;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Feature/GoToImplementation/FindClassLikeImplementationsVisitor.php:
--------------------------------------------------------------------------------
1 | root = $root;
24 | }
25 |
26 | public function enter(ResolvedClassLike $class): \Generator
27 | {
28 | if ($class->location != $this->root->location) {
29 | $this->implementations[] = $class;
30 | }
31 |
32 | return;
33 | yield;
34 | }
35 |
36 | public function leave(ResolvedClassLike $class): \Generator
37 | {
38 | return;
39 | yield;
40 | }
41 |
42 | /**
43 | * @return ResolvedClassLike[]
44 | */
45 | public function getImplementations(): array
46 | {
47 | // deduplicate
48 | $result = [];
49 | foreach ($this->implementations as $impl) {
50 | foreach ($result as $prevImpl) {
51 | if ($impl->location == $prevImpl->location) {
52 | continue 2;
53 | }
54 | }
55 | $result[] = $impl;
56 | }
57 |
58 | return $result;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Feature/Hover/ExpressionTypeHoverProvider.php:
--------------------------------------------------------------------------------
1 | typeInference = $typeInference;
35 | $this->nodeFinder = $nodeFinder;
36 | $this->formatter = $formatter;
37 | }
38 |
39 | public function getHover(Document $document, Position $position): \Generator
40 | {
41 | if ($document->getLanguage() !== 'php') {
42 | return null;
43 | }
44 |
45 | /** @var (Node|Comment)[] $nodes */
46 | $nodes = yield $this->nodeFinder->getNodePath($document, $position);
47 | yield $this->typeInference->infer($document, $nodes);
48 |
49 | if (!empty($nodes) && $nodes[0] instanceof Node\Name) {
50 | array_shift($nodes);
51 | }
52 |
53 | if (!empty($nodes) && $nodes[0] instanceof Node) {
54 | $type = $nodes[0]->getAttribute('type', null);
55 | if ($type !== null) {
56 | $hover = new Hover();
57 | $hover->contents = $this->formatter->formatExpression($type);
58 | $hover->range = PositionUtils::rangeFromNodeAttrs($nodes[0]->getAttributes(), $document);
59 |
60 | return $hover;
61 | }
62 | }
63 |
64 | return null;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Feature/Refactoring/Differ.php:
--------------------------------------------------------------------------------
1 | tabs = $tabs;
20 | $this->spaces = $spaces;
21 | }
22 |
23 | public function render(): string
24 | {
25 | return str_repeat("\t", $this->tabs) . str_repeat(' ', $this->spaces);
26 | }
27 |
28 | public static function min(self $a, self $b): self
29 | {
30 | if ($a->tabs === $b->tabs) {
31 | return $a->spaces < $b->spaces ? $a : $b;
32 | }
33 |
34 | return $a->tabs < $b->tabs ? $a : $b;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Feature/Refactoring/WorkspaceEditCommandProvider.php:
--------------------------------------------------------------------------------
1 | workspaceEditFeature = $workspaceEditFeature;
21 | }
22 |
23 | public function getCommand(): string
24 | {
25 | return self::COMMAND;
26 | }
27 |
28 | public function execute(string $label, WorkspaceEdit $edit): \Generator
29 | {
30 | yield $this->workspaceEditFeature->applyWorkspaceEdit($label, $edit);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Feature/References/Reference.php:
--------------------------------------------------------------------------------
1 | nodesToTag = $nodesToTag;
23 | }
24 |
25 | public function enterNode(Node $node)
26 | {
27 | parent::enterNode($node);
28 |
29 | if ($this->nodesToTag->contains($node)) {
30 | $node->setAttribute('nameContext', clone $this->nameContext);
31 | }
32 |
33 | return null;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Parser/Ast.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
22 | }
23 |
24 | public function getDiagnostics(Document $document): \Generator
25 | {
26 | if ($document->getLanguage() !== 'php') {
27 | return [];
28 | }
29 |
30 | /** @var Ast $ast */
31 | $ast = yield $this->parser->parse($document);
32 |
33 | return array_map(function (Error $error) use ($document) {
34 | $diag = new Diagnostic();
35 | $diag->range = PositionUtils::rangeFromNodeAttrs($error->getAttributes(), $document);
36 | $diag->severity = DiagnosticSeverity::ERROR;
37 | $diag->source = 'php';
38 | $diag->message = $error->getRawMessage();
39 |
40 | return $diag;
41 | }, $ast->errors);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/PhpStan/Analyser/AnalysedCacheAware.php:
--------------------------------------------------------------------------------
1 | methodReflection = $methodReflection;
15 | }
16 |
17 | public function isGenerator()
18 | {
19 | return $this->methodReflection->isGenerator();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/PhpStan/IndexReflection/DummyReflectionProperty.php:
--------------------------------------------------------------------------------
1 | propertyReflection = $propertyReflection;
22 | $this->propertyName = $name;
23 | }
24 |
25 | public function getName()
26 | {
27 | return $this->propertyName;
28 | }
29 |
30 | public function getDeclaringClass()
31 | {
32 | $declaringClass = $this->propertyReflection->getDeclaringClass();
33 | assert($declaringClass instanceof IndexClassReflection);
34 |
35 | return new DummyReflectionClass($declaringClass);
36 | }
37 |
38 | public function isPrivate()
39 | {
40 | return $this->propertyReflection->isPrivate();
41 | }
42 |
43 | public function isProtected()
44 | {
45 | return !$this->propertyReflection->isPublic() && !$this->propertyReflection->isPrivate();
46 | }
47 |
48 | public function isStatic()
49 | {
50 | return $this->propertyReflection->isStatic();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/PhpStan/IndexReflection/DummyReflectionType.php:
--------------------------------------------------------------------------------
1 | string = ltrim($string, '\\');
30 | $this->allowsNull = $allowsNull;
31 | $this->isBuiltin = $isBuiltin;
32 | }
33 |
34 | public function allowsNull()
35 | {
36 | return $this->allowsNull;
37 | }
38 |
39 | public function isBuiltin()
40 | {
41 | return $this->isBuiltin;
42 | }
43 |
44 | public function __toString()
45 | {
46 | return $this->string;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/PhpStan/PhpDocResolver/PhpDocResolverVisitor.php:
--------------------------------------------------------------------------------
1 | comment => name context
13 | */
14 | private $nameContexts = [];
15 |
16 | /**
17 | * @var NameContext|null
18 | */
19 | private $lastNameContext;
20 |
21 | public function enterNode(Node $node)
22 | {
23 | parent::enterNode($node);
24 |
25 | $phpDoc = $node->getDocComment();
26 | if ($phpDoc !== null) {
27 | if ($this->lastNameContext === null || $this->lastNameContext != $this->nameContext) {
28 | $this->lastNameContext = clone $this->nameContext;
29 | }
30 | $this->nameContexts[$phpDoc->getText()] = $this->lastNameContext;
31 | }
32 | }
33 |
34 | /**
35 | * @return array comment => name context
36 | */
37 | public function getNameContexts(): array
38 | {
39 | return $this->nameContexts;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/PhpStan/Utils/DocumentParser.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
34 | $this->syncAsync = $syncAsync;
35 | }
36 |
37 | public function setDocument(?Document $document): void
38 | {
39 | $this->document = $document;
40 | }
41 |
42 | public function parseFile(string $file): array
43 | {
44 | if ($this->document === null) {
45 | throw new ShouldNotHappenException();
46 | }
47 |
48 | if (!$this->document->getUri()->equals(Uri::fromFilesystemPath($file))) {
49 | throw new ShouldNotHappenException();
50 | }
51 |
52 | /** @var Ast $ast */
53 | $ast = $this->syncAsync->callAsync($this->parser->parse($this->document));
54 |
55 | return $ast->nodes;
56 | }
57 |
58 | public function parseString(string $sourceCode): array
59 | {
60 | throw new ShouldNotHappenException();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/PhpStan/Utils/ErrorTolerantPrettyPrinter.php:
--------------------------------------------------------------------------------
1 | getConstantValue($name);
14 | }
15 |
16 | return \constant($name);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Reflection/ClassResolverExtension.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
31 | $this->reflectionProvider = $reflectionProvider;
32 | $this->classResolver = $classResolver;
33 | }
34 |
35 | /**
36 | * @resolve mixed
37 | */
38 | public function evaluate(string $expr, NameContext $nameContext, Document $document): \Generator
39 | {
40 | return yield (new ConstExprEvaluation(
41 | $this->parser,
42 | $this->reflectionProvider,
43 | $this->classResolver,
44 | $document
45 | ))->evaluate($expr, $nameContext);
46 | }
47 |
48 | /**
49 | * @resolve mixed
50 | */
51 | public function getConstValue(Element\Const_ $const, Document $document): \Generator
52 | {
53 | return yield (new ConstExprEvaluation(
54 | $this->parser,
55 | $this->reflectionProvider,
56 | $this->classResolver,
57 | $document
58 | ))->getConstValue($const);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Reflection/Element/ClassConst.php:
--------------------------------------------------------------------------------
1 | location === null) {
45 | return null;
46 | }
47 |
48 | $link = new LocationLink();
49 | $link->originSelectionRange = $originSelectionRange;
50 | $link->targetUri = $this->location->uri;
51 | $link->targetRange = $this->location->range;
52 | $link->targetSelectionRange = $this->nameRange ?? $this->location->range;
53 |
54 | return $link;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Reflection/Element/Function_.php:
--------------------------------------------------------------------------------
1 | classResolver = $classResolver;
23 | $this->reflectionProvider = $reflectionProvider;
24 | }
25 |
26 | /**
27 | * @param InheritanceTreeVisitor[] $visitors
28 | */
29 | public function traverse(string $className, array $visitors, Document $document): \Generator
30 | {
31 | yield $this->traverseClass($className, $visitors, $document, new Cache());
32 | }
33 |
34 | /**
35 | * @param InheritanceTreeVisitor[] $visitors
36 | */
37 | private function traverseClass(string $className, array $visitors, Document $document, Cache $cache): \Generator
38 | {
39 | $class = yield $this->classResolver->resolve($className, $document, $cache);
40 | if ($class === null) {
41 | return;
42 | }
43 |
44 | foreach ($visitors as $visitor) {
45 | yield $visitor->enter($class);
46 | }
47 |
48 | foreach (yield $this->reflectionProvider->getInheritingClasses($document, $className) as $inheritingClassName) {
49 | yield $this->traverseClass($inheritingClassName, $visitors, $document, $cache);
50 | }
51 |
52 | foreach ($visitors as $visitor) {
53 | yield $visitor->leave($class);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Reflection/InheritanceTreeVisitor.php:
--------------------------------------------------------------------------------
1 | withLineNumber($node->getLine());
21 |
22 | return self::ANONYMOUS_CLASS_PREFIX . sha1($uri->getNormalized());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/Reflection/ReflectionTransformer.php:
--------------------------------------------------------------------------------
1 | description;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/TypeInference/IntersectionType.php:
--------------------------------------------------------------------------------
1 | types);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/TypeInference/ObjectType.php:
--------------------------------------------------------------------------------
1 | class = $class;
12 | }
13 |
14 | public function __toString(): string
15 | {
16 | return $this->class;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Php/TypeInference/Type.php:
--------------------------------------------------------------------------------
1 | types);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/PhpUnit/MockBuilderDynamicReturnTypeExtension.php:
--------------------------------------------------------------------------------
1 | setClass(DynamicMethodReturnTypeExtension::class, CreateMockDynamicReturnTypeExtension::class, true);
26 | $container->setClass(DynamicMethodReturnTypeExtension::class, GetMockBuilderDynamicReturnTypeExtension::class, true);
27 | $container->setClass(DynamicMethodReturnTypeExtension::class, MockBuilderDynamicReturnTypeExtension::class, true);
28 | $container->setClass(FunctionTypeSpecifyingExtension::class, AssertFunctionTypeSpecifyingExtension::class, true);
29 | $container->setClass(MethodTypeSpecifyingExtension::class, AssertMethodTypeSpecifyingExtension::class, true);
30 | $container->setClass(StaticMethodTypeSpecifyingExtension::class, AssertStaticMethodTypeSpecifyingExtension::class, true);
31 |
32 | $container->setClass(Rule::class, AssertSameBooleanExpectedRule::class, true);
33 | $container->setClass(Rule::class, AssertSameNullExpectedRule::class, true);
34 | $container->setClass(Rule::class, AssertSameWithCountRule::class, true);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Prophecy/ProphecyPlugin.php:
--------------------------------------------------------------------------------
1 | setClass(DynamicMethodReturnTypeExtension::class, ObjectProphecyRevealDynamicReturnTypeExtension::class, true);
19 | $container->setClass(DynamicMethodReturnTypeExtension::class, TestCaseProphesizeDynamicReturnTypeExtension::class, true);
20 | $container->setClass(DynamicMethodReturnTypeExtension::class, ProphetProphesizeDynamicReturnTypeExtension::class, true);
21 | $container->setClass(MethodsClassReflectionExtension::class, ProphecyMethodsClassReflectionExtension::class, true);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Document/Document.php:
--------------------------------------------------------------------------------
1 | uri = $uri;
35 | $this->language = $language;
36 | }
37 |
38 | public function getUri(): Uri
39 | {
40 | return $this->uri;
41 | }
42 |
43 | public function getVersion(): ?int
44 | {
45 | return $this->version;
46 | }
47 |
48 | public function getLanguage(): string
49 | {
50 | return $this->language;
51 | }
52 |
53 | public function getText(): string
54 | {
55 | return $this->text;
56 | }
57 |
58 | public function update(string $text, ?int $version = null): self
59 | {
60 | $this->text = $text;
61 | $this->version = $version;
62 | $this->data = [];
63 |
64 | return $this;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Document/Project.php:
--------------------------------------------------------------------------------
1 | rootUri = $rootUri;
20 | }
21 |
22 | public function getRootUri(): Uri
23 | {
24 | return $this->rootUri;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Event/Document/OnChange.php:
--------------------------------------------------------------------------------
1 | container = $container;
23 | $this->timeout = 120.0;
24 | }
25 |
26 | /**
27 | * Dispatch coroutines in a new strand and return immediately.
28 | */
29 | public function dispatch(string $event, ...$args): \Generator
30 | {
31 | yield Recoil::execute(Recoil::timeout($this->timeout, $this->dispatchAndWait($event, ...$args)));
32 | }
33 |
34 | /**
35 | * Dispatch and wait until all dispatched coroutines finish.
36 | */
37 | public function dispatchAndWait(string $event, ...$args): \Generator
38 | {
39 | $parts = explode('\\', $event);
40 | $method = end($parts);
41 | $listeners = $this->container->getOrDefault($event, []);
42 |
43 | yield array_map(function ($listener) use ($method, $args) {
44 | return $listener->$method(...$args);
45 | }, $listeners);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Event/OnFileChange.php:
--------------------------------------------------------------------------------
1 | uri = $this->targetUri;
47 | $location->range = $this->targetRange;
48 |
49 | return $location;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Feature/Common/MarkupContent.php:
--------------------------------------------------------------------------------
1 | line = $line;
43 | $this->character = $character;
44 | }
45 |
46 | public function __toString(): string
47 | {
48 | return "($this->line,$this->character)";
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Feature/Common/Range.php:
--------------------------------------------------------------------------------
1 | start = $start;
33 | $this->end = $end;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Feature/Common/SymbolInformation.php:
--------------------------------------------------------------------------------
1 | |null URI => TextEdit[].
18 | */
19 | public $changes;
20 |
21 | /**
22 | * An array of `TextDocumentEdit`s to express changes to n different text documents
23 | *
24 | * ...where each text document edit addresses a specific version of a text
25 | * document. Whether a client supports versioned document edits is
26 | * expressed via `WorkspaceClientCapabilities.workspaceEdit.documentChanges`.
27 | *
28 | * @var TextDocumentEdit[]|null
29 | */
30 | public $documentChanges;
31 | }
32 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Feature/Completion/CompletionContext.php:
--------------------------------------------------------------------------------
1 | items = array_merge(...array_map(function (CompletionList $list) {
37 | return array_values($list->items);
38 | }, $completionsLists));
39 |
40 | $completions->isIncomplete = 0 !== array_sum(array_map(function (CompletionList $list) {
41 | return $list->isIncomplete;
42 | }, $completionsLists));
43 |
44 | return $completions;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Feature/Completion/CompletionProvider.php:
--------------------------------------------------------------------------------
1 | URI => diagnostics
13 | */
14 | public function getWorkspaceDiagnostics(array $documents): \Generator;
15 | }
16 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Feature/DocumentSymbols/DocumentSymbol.php:
--------------------------------------------------------------------------------
1 | progressGroup = $progressGroup;
20 | }
21 |
22 | public function __destruct()
23 | {
24 | $this->done();
25 | }
26 |
27 | public function set(string $label, ?int $status = null): void
28 | {
29 | $this->progressGroup->progress($label, $status);
30 | }
31 |
32 | public function done(): void
33 | {
34 | if (!$this->done) {
35 | $this->done = true;
36 | $this->progressGroup->done();
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Feature/ProgressNotification/ProgressGroup.php:
--------------------------------------------------------------------------------
1 | id = $id;
25 | $this->progressCallback = $progressCallback;
26 | }
27 |
28 | public function get(): Progress
29 | {
30 | $this->activeCount++;
31 |
32 | return new Progress($this);
33 | }
34 |
35 | /**
36 | * @internal
37 | */
38 | public function progress(string $label, ?int $status = null): void
39 | {
40 | ($this->progressCallback)($this->id, $label, $status);
41 | }
42 |
43 | /**
44 | * @internal
45 | */
46 | public function done(): void
47 | {
48 | $this->activeCount--;
49 | if ($this->activeCount <= 0) {
50 | ($this->progressCallback)($this->id, null, null, true);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Feature/ProgressNotification/ProgressNotificationFeature.php:
--------------------------------------------------------------------------------
1 | rpc = $rpc;
29 | $this->kernel = $kernel;
30 | }
31 |
32 | public function initialize(ClientCapabilities $clientCapabilities, ServerCapabilities $serverCapabilities): \Generator
33 | {
34 | return;
35 | yield;
36 | }
37 |
38 | public function create(): ProgressGroup
39 | {
40 | $callback = function (string $id, ?string $label = null, ?int $status = null, bool $done = false) {
41 | $this->kernel->execute($this->progress($id, $label, $status, $done));
42 | };
43 |
44 | return new ProgressGroup($this->generateId(), $callback);
45 | }
46 |
47 | private function progress(string $id, ?string $label = null, ?int $status = null, bool $done = false): \Generator
48 | {
49 | if ($this->rpc !== null) {
50 | yield $this->rpc->notify('$/tenkawaphp/window/progress', compact('id', 'label', 'status', 'done'));
51 | }
52 | }
53 |
54 | private function generateId(): string
55 | {
56 | return uniqid('', true);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Feature/References/ReferenceContext.php:
--------------------------------------------------------------------------------
1 | rpc = $rpc;
29 | $this->logger = $logger;
30 | }
31 |
32 | public function initialize(ClientCapabilities $clientCapabilities, ServerCapabilities $serverCapabilities): \Generator
33 | {
34 | return;
35 | yield;
36 | }
37 |
38 | /**
39 | * The workspace/applyEdit request is sent from the server to the client to
40 | * modify resource on the client side.
41 | *
42 | * @param string|null $label An optional label of the workspace edit.
43 | * This label is presented in the user interface
44 | * for example on an undo stack to undo the workspace edit.
45 | * @param WorkspaceEdit $edit The edits to apply.
46 | *
47 | * @resolve ApplyWorkspaceEditResponse
48 | */
49 | public function applyWorkspaceEdit(?string $label, WorkspaceEdit $edit): \Generator
50 | {
51 | $this->logger->debug('send: ' . __FUNCTION__);
52 |
53 | return yield $this->rpc->call('workspace/applyEdit', compact('label', 'edit'), ApplyWorkspaceEditResponse::class);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Feature/WorkspaceSymbols/WorkspaceSymbolsProvider.php:
--------------------------------------------------------------------------------
1 | documentStore = $documentStore;
20 | }
21 |
22 | /**
23 | * @param Document|Project $documentOrProject
24 | *
25 | * @resolve IndexEntry[]
26 | */
27 | public function search($documentOrProject, Query $query, bool $projectOnly = false): \Generator
28 | {
29 | if ($documentOrProject instanceof Document) {
30 | /** @var Project $project */
31 | $project = yield $this->documentStore->getProjectForDocument($documentOrProject);
32 | } else {
33 | $project = $documentOrProject;
34 | }
35 |
36 | /** @var IndexStorage $indexStorage */
37 | $indexStorage = $project->get('index' . ($projectOnly ? '.project_only' : ''));
38 |
39 | return yield $indexStorage->search($query);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Index/IndexDataProvider.php:
--------------------------------------------------------------------------------
1 | cacheDir = $dirs->getCacheDir() . '/index';
28 | $this->documentStore = $documentStore;
29 | }
30 |
31 | public function createOpenedFilesIndex(Project $project, string $indexDataVersion): WritableIndexStorage
32 | {
33 | return new OpenDocumentsStorage($project, $this->documentStore);
34 | }
35 |
36 | public function createProjectFilesIndex(Project $project, string $indexDataVersion): WritableIndexStorage
37 | {
38 | $hash = sha1((string)$project->getRootUri());
39 |
40 | return new SqliteStorage(
41 | $this->cacheDir . "/project-$hash.sqlite",
42 | $indexDataVersion,
43 | (string)$project->getRootUri()
44 | );
45 | }
46 |
47 | public function createStubsIndex(Uri $uri, string $indexDataVersion): WritableIndexStorage
48 | {
49 | $hash = sha1((string)$uri);
50 |
51 | return new SqliteStorage(
52 | $this->cacheDir . "/stubs-$hash.sqlite",
53 | $indexDataVersion,
54 | (string)$uri
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Index/MemoryIndexStorageFactory.php:
--------------------------------------------------------------------------------
1 | documentStore = $documentStore;
22 | }
23 |
24 | public function createOpenedFilesIndex(Project $project, string $indexDataVersion): WritableIndexStorage
25 | {
26 | return new OpenDocumentsStorage($project, $this->documentStore);
27 | }
28 |
29 | public function createProjectFilesIndex(Project $project, string $indexDataVersion): WritableIndexStorage
30 | {
31 | return new MemoryStorage();
32 | }
33 |
34 | public function createStubsIndex(Uri $uri, string $indexDataVersion): WritableIndexStorage
35 | {
36 | return new MemoryStorage();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Index/Query.php:
--------------------------------------------------------------------------------
1 | primaryStorage = $primaryStorage;
29 | $this->secondaryStorage = $secondaryStorage;
30 | }
31 |
32 | public function search(Query $query): \Generator
33 | {
34 | $result = yield $this->primaryStorage->search($query);
35 | $primaryFiles = yield $this->primaryStorage->getFileStamps();
36 |
37 | /** @var IndexEntry $entry */
38 | foreach (yield $this->secondaryStorage->search($query) as $entry) {
39 | if (!array_key_exists($entry->sourceUri->getNormalized(), $primaryFiles)) {
40 | $result[] = $entry;
41 | }
42 | }
43 |
44 | return $result;
45 | }
46 |
47 | public function getFileStamps(?Uri $filterUri = null): \Generator
48 | {
49 | return array_merge(
50 | yield $this->secondaryStorage->getFileStamps($filterUri),
51 | yield $this->primaryStorage->getFileStamps($filterUri)
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Index/Storage/IndexStorage.php:
--------------------------------------------------------------------------------
1 | string URI => ?string stamp
20 | */
21 | public function getFileStamps(?Uri $filterUri = null): \Generator;
22 | }
23 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Index/Storage/MergedStorage.php:
--------------------------------------------------------------------------------
1 | innerStorage = $innerStorage;
24 | }
25 |
26 | public function search(Query $query): \Generator
27 | {
28 | $result = [];
29 |
30 | foreach ($this->innerStorage as $storage) {
31 | $result = array_merge($result, yield $storage->search($query));
32 | }
33 |
34 | return $result;
35 | }
36 |
37 | public function getFileStamps(?Uri $filterUri = null): \Generator
38 | {
39 | $result = [];
40 |
41 | foreach ($this->innerStorage as $storage) {
42 | $result = array_merge($result, yield $storage->getFileStamps($filterUri));
43 | }
44 |
45 | return $result;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Index/Storage/WritableIndexStorage.php:
--------------------------------------------------------------------------------
1 | [string $fileType, string $stamp]
14 | */
15 | public function list(Uri $uri, array $filters, ?Uri $baseUri = null): \Generator;
16 | }
17 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Io/FileLister/GlobFileFilter.php:
--------------------------------------------------------------------------------
1 | glob = $glob;
29 | $this->fileType = $fileType;
30 | $this->action = $action;
31 | }
32 |
33 | public function filter(string $uri, string $baseUri): int
34 | {
35 | if (Glob::match($uri, Path::join($baseUri, $this->glob))) {
36 | return $this->action;
37 | }
38 |
39 | return self::ABSTAIN;
40 | }
41 |
42 | public function getFileType(): string
43 | {
44 | return $this->fileType;
45 | }
46 |
47 | public function enterDirectory(string $uri, string $baseUri): int
48 | {
49 | $dir = Glob::getBasePath(Path::join($baseUri, $this->glob));
50 | if ($dir === $uri || StringUtils::startsWith($uri, $dir . '/') || StringUtils::startsWith($dir, $uri . '/')) {
51 | return self::ACCEPT;
52 | }
53 |
54 | return self::ABSTAIN;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Io/FileLister/GlobRejectDirectoryFilter.php:
--------------------------------------------------------------------------------
1 | glob = $glob;
18 | }
19 |
20 | public function filter(string $uri, string $baseUri): int
21 | {
22 | if (Glob::match($uri, Path::join($baseUri, $this->glob, '**/*'))) {
23 | return self::REJECT;
24 | }
25 |
26 | return self::ABSTAIN;
27 | }
28 |
29 | public function getFileType(): string
30 | {
31 | return '';
32 | }
33 |
34 | public function enterDirectory(string $uri, string $baseUri): int
35 | {
36 | if (Glob::match($uri, Path::join($baseUri, $this->glob . '{,/**/*}'))) {
37 | return self::REJECT;
38 | }
39 |
40 | return self::ABSTAIN;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Io/FileReader.php:
--------------------------------------------------------------------------------
1 | eventDispatcher = $eventDispatcher;
22 | }
23 |
24 | public function onClose(Document $document): \Generator
25 | {
26 | yield Recoil::sleep(2.0);
27 | yield $this->eventDispatcher->dispatch(OnFileChange::class, [$document->getUri()]);
28 | }
29 |
30 | public function isAvailable(): bool
31 | {
32 | return true;
33 | }
34 |
35 | public function start(): \Generator
36 | {
37 | return;
38 | yield;
39 | }
40 |
41 | public function stop(): \Generator
42 | {
43 | return;
44 | yield;
45 | }
46 |
47 | public function addDirectory(Uri $uri): \Generator
48 | {
49 | return;
50 | yield;
51 | }
52 |
53 | public function removeDirectory(Uri $uri): \Generator
54 | {
55 | return;
56 | yield;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Io/FileWatcher/FileChangeDeduplicator.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | private $uris = [];
26 |
27 | /**
28 | * @var bool
29 | */
30 | private $accumulating = false;
31 |
32 | public function __construct(EventDispatcher $eventDispatcher, float $accumulateTime)
33 | {
34 | $this->eventDispatcher = $eventDispatcher;
35 | $this->accumulateTime = $accumulateTime;
36 | }
37 |
38 | /**
39 | * @param Uri[] $uris
40 | */
41 | public function dispatch(array $uris): \Generator
42 | {
43 | foreach ($uris as $uri) {
44 | $this->uris[$uri->getNormalizedWithSlash()] = $uri;
45 | }
46 |
47 | if (!$this->accumulating) {
48 | $this->accumulating = true;
49 |
50 | try {
51 | yield Recoil::sleep($this->accumulateTime);
52 | $this->deduplicate();
53 | yield $this->eventDispatcher->dispatch(OnFileChange::class, array_values($this->uris));
54 | } finally {
55 | $this->uris = [];
56 | $this->accumulating = false;
57 | }
58 | }
59 | }
60 |
61 | private function deduplicate(): void
62 | {
63 | ksort($this->uris);
64 | /** @var Uri|null $prevUri */
65 | $prevUri = null;
66 | foreach ($this->uris as $key => $uri) {
67 | if ($prevUri !== null && ($prevUri->equals($uri) || $prevUri->isParentOf($uri))) {
68 | unset($this->uris[$key]);
69 | } else {
70 | $prevUri = $uri;
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Io/FileWatcher/FileWatcher.php:
--------------------------------------------------------------------------------
1 | throttler = new Throttler(self::MAX_CONCURRENT);
24 | }
25 |
26 | public function read(Uri $uri): \Generator
27 | {
28 | $job = function () use ($uri): \Generator {
29 | $file = false;
30 |
31 | try {
32 | $file = @fopen($uri->getFilesystemPath(), 'r');
33 | if ($file === false) {
34 | throw new IoException("Can't open file $uri");
35 | }
36 |
37 | stream_set_blocking($file, false);
38 |
39 | $content = yield Recoil::read($file, self::MAX_SIZE + 1, self::MAX_SIZE + 1);
40 | if (strlen($content) > self::MAX_SIZE) {
41 | throw new IoException("File size limit exceeded for $uri");
42 | }
43 | } finally {
44 | if (is_resource($file)) {
45 | @fclose($file);
46 | }
47 | }
48 |
49 | return $content;
50 | };
51 |
52 | return yield $this->throttler->run($job);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Logger/ClientLogger.php:
--------------------------------------------------------------------------------
1 | MessageType::ERROR,
17 | LogLevel::ALERT => MessageType::ERROR,
18 | LogLevel::CRITICAL => MessageType::ERROR,
19 | LogLevel::ERROR => MessageType::ERROR,
20 | LogLevel::WARNING => MessageType::WARNING,
21 | LogLevel::NOTICE => MessageType::WARNING,
22 | LogLevel::INFO => MessageType::INFO,
23 | LogLevel::DEBUG => MessageType::LOG,
24 | ];
25 |
26 | /**
27 | * @var MessageFeature
28 | */
29 | private $messageFeature;
30 |
31 | /**
32 | * @var Kernel
33 | */
34 | private $kernel;
35 |
36 | public function __construct(MessageFeature $messageFeature, Kernel $kernel)
37 | {
38 | $this->messageFeature = $messageFeature;
39 | $this->kernel = $kernel;
40 | }
41 |
42 | public function log($level, $message, array $context = [])
43 | {
44 | $this->kernel->execute(function () use ($level, $message, $context) {
45 | yield $this->messageFeature->logMessage(
46 | static::LEVEL_MAP[$level],
47 | trim($this->interpolate($message, $context) . "\n" . ($context['exception'] ?? ''))
48 | );
49 | });
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Logger/CompositeLogger.php:
--------------------------------------------------------------------------------
1 | loggers as $logger) {
18 | $logger->log($level, $message, $context);
19 | }
20 | }
21 |
22 | public function add(LoggerInterface $logger): self
23 | {
24 | $this->loggers[] = $logger;
25 |
26 | return $this;
27 | }
28 |
29 | public function clear(): self
30 | {
31 | $this->loggers = [];
32 |
33 | return $this;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Logger/LevelFilteringLogger.php:
--------------------------------------------------------------------------------
1 | 8,
13 | LogLevel::ALERT => 7,
14 | LogLevel::CRITICAL => 6,
15 | LogLevel::ERROR => 5,
16 | LogLevel::WARNING => 4,
17 | LogLevel::NOTICE => 3,
18 | LogLevel::INFO => 2,
19 | LogLevel::DEBUG => 1,
20 | ];
21 |
22 | /**
23 | * @var LoggerInterface
24 | */
25 | private $inner;
26 |
27 | /**
28 | * @var int
29 | */
30 | private $levelNumber;
31 |
32 | public function __construct(LoggerInterface $inner, string $level)
33 | {
34 | $this->inner = $inner;
35 | $this->levelNumber = $this->getLevelNumber($level);
36 | }
37 |
38 | private function getLevelNumber(string $level): int
39 | {
40 | return self::LEVEL_NUMBER[$level] ?? self::LEVEL_NUMBER[LogLevel::DEBUG];
41 | }
42 |
43 | public function log($level, $message, array $context = [])
44 | {
45 | if ($this->getLevelNumber($level) >= $this->levelNumber) {
46 | $this->inner->log($level, $message, $context);
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Logger/LoggerTrait.php:
--------------------------------------------------------------------------------
1 | $val) {
11 | if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
12 | $replace['{' . $key . '}'] = $val;
13 | }
14 | }
15 |
16 | return strtr($message, $replace);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Logger/StreamLogger.php:
--------------------------------------------------------------------------------
1 | stream = $stream;
22 | }
23 |
24 | public function log($level, $message, array $context = [])
25 | {
26 | $context['date'] = date(\DateTime::ATOM);
27 | $context['pid'] = (string)getmypid();
28 | $context['level'] = strtoupper($level);
29 | $context['exception'] = isset($context['exception']) ? strtr((string)$context['exception'], ["\n" => "\n "]) : '';
30 |
31 | fwrite($this->stream, trim($this->interpolate("{date} pid={pid} {level} $message\n {exception}", $context)) . "\n");
32 | fflush($this->stream);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Mapper/UriMapper.php:
--------------------------------------------------------------------------------
1 | getMessage());
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Plugin.php:
--------------------------------------------------------------------------------
1 | get()` or otherwize freeze the
13 | * container.
14 | */
15 | public function configureContainer(Container $container, array $options): void
16 | {
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/PluginFinder.php:
--------------------------------------------------------------------------------
1 | methodRegistry = $methodRegistry;
45 | $this->methodProviders = $methodProviders;
46 | $this->logger = $logger;
47 | $this->clientLogger = $clientLogger;
48 | }
49 |
50 | public function onStart(array $options): \Generator
51 | {
52 | if ($this->methodRegistry instanceof SimpleMethodRegistry) {
53 | foreach ($this->methodProviders as $provider) {
54 | $this->methodRegistry->addProvider($provider);
55 | }
56 | }
57 |
58 | if ($this->logger instanceof CompositeLogger) {
59 | if ($options['log.client'] ?? false) {
60 | $this->logger->add($this->clientLogger);
61 | }
62 | }
63 |
64 | return;
65 | yield;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Transport/RunnableTransport.php:
--------------------------------------------------------------------------------
1 | send($args);
30 | };
31 |
32 | $errorListener = function ($error) use ($strand, &$removeListeners) {
33 | $removeListeners();
34 | $strand->throw($error);
35 | };
36 |
37 | $removeListeners = function () use ($emitter, $events, $errorEvents, $listener, $errorListener) {
38 | foreach ($events as $event) {
39 | $emitter->removeListener($event, $listener);
40 | }
41 | foreach ($errorEvents as $event) {
42 | $emitter->removeListener($event, $errorListener);
43 | }
44 | };
45 |
46 | foreach ($events as $event) {
47 | $emitter->on($event, $listener);
48 | }
49 | foreach ($errorEvents as $event) {
50 | $emitter->on($event, $errorListener);
51 | }
52 | });
53 | }
54 |
55 | // @codeCoverageIgnoreStart
56 | private function __construct()
57 | {
58 | }
59 |
60 | // @codeCoverageIgnoreEnd
61 | }
62 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/FuzzyMatcher.php:
--------------------------------------------------------------------------------
1 | data[$key] ?? null;
20 | }
21 |
22 | public function set(string $key, $data): self
23 | {
24 | $this->data[$key] = $data;
25 |
26 | return $this;
27 | }
28 |
29 | public function clear(): self
30 | {
31 | $this->data = [];
32 |
33 | return $this;
34 | }
35 |
36 | public function isClosed(): bool
37 | {
38 | return $this->closed;
39 | }
40 |
41 | public function close(): void
42 | {
43 | $this->closed = true;
44 | $this->clear();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/NestedKernelsSyncAsync.php:
--------------------------------------------------------------------------------
1 | kernelFactory = $kernelFactory;
27 | }
28 |
29 | public function callSync(
30 | callable $syncCallable,
31 | array $args = [],
32 | ?callable $resumeCallback = null,
33 | ?callable $pauseCallback = null
34 | ) {
35 | $context = new SyncCallContext();
36 | $context->resumeCallback = $resumeCallback;
37 | $context->pauseCallback = $pauseCallback;
38 |
39 | $this->syncStack[] = $context;
40 | $context->resume();
41 |
42 | try {
43 | $result = $syncCallable(...$args);
44 | } finally {
45 | $context->pause();
46 | array_pop($this->syncStack);
47 | }
48 |
49 | return $result;
50 | }
51 |
52 | public function callAsync(\Generator $coroutine)
53 | {
54 | assert(!empty($this->syncStack));
55 | $context = $this->syncStack[count($this->syncStack) - 1];
56 | $context->pause();
57 |
58 | $kernel = $this->cachedKernel ?? ($this->kernelFactory)();
59 | $this->cachedKernel = null;
60 |
61 | try {
62 | $result = $kernel->start($coroutine);
63 | } finally {
64 | $context->resume();
65 | $this->cachedKernel = $kernel;
66 | }
67 |
68 | return $result;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/Platform.php:
--------------------------------------------------------------------------------
1 | setPriority($priority);
18 | }
19 |
20 | yield;
21 | }
22 |
23 | public static function interactive(int $bonus = 0): \Generator
24 | {
25 | return self::set(self::INTERACTIVE + $bonus);
26 | }
27 |
28 | public static function foreground(int $bonus = 0): \Generator
29 | {
30 | return self::set(self::FOREGROUND + $bonus);
31 | }
32 |
33 | public static function background(int $bonus = 0): \Generator
34 | {
35 | return self::set(self::BACKGROUND + $bonus);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/PriorityKernel/PriorityStrand.php:
--------------------------------------------------------------------------------
1 | priority;
20 | }
21 |
22 | /**
23 | * @return $this
24 | */
25 | public function setPriority(int $priority): self
26 | {
27 | $this->priority = $priority;
28 |
29 | return $this;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/PriorityKernel/ScheduledReactKernel.php:
--------------------------------------------------------------------------------
1 | eventLoop = $eventLoop;
37 | $this->api = $api;
38 | $this->scheduler = $scheduler;
39 | $this->panicExceptions = new SplQueue();
40 | }
41 |
42 | public static function create(?LoopInterface $eventLoop = null, ?Scheduler $scheduler = null): self
43 | {
44 | if ($eventLoop === null) {
45 | $eventLoop = Factory::create();
46 | }
47 |
48 | if ($scheduler === null) {
49 | $scheduler = new ReactScheduler($eventLoop);
50 | }
51 |
52 | return new self(
53 | $eventLoop,
54 | new ScheduledApi(new ReactApi($eventLoop), $scheduler),
55 | $scheduler
56 | );
57 | }
58 |
59 | public function execute($coroutine): Strand
60 | {
61 | $strand = new PriorityStrand($this, $this->api, $this->nextId++, $coroutine);
62 | $this->scheduler->scheduleStart($strand);
63 |
64 | return $strand;
65 | }
66 |
67 | public function stop()
68 | {
69 | /** @var int $state */
70 | $state = &$this->state; // To override wrong annotation in KernelTrait
71 | if ($state === KernelState::RUNNING) {
72 | $state = KernelState::STOPPING;
73 | $this->eventLoop->stop();
74 | }
75 | }
76 |
77 | protected function loop()
78 | {
79 | $this->eventLoop->run();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/PriorityKernel/Scheduler.php:
--------------------------------------------------------------------------------
1 | start = $this->now();
20 | $this->scale = $scale;
21 | }
22 |
23 | public function getSeconds(): float
24 | {
25 | return $this->now() - $this->start;
26 | }
27 |
28 | public function __toString(): string
29 | {
30 | $seconds = $this->getSeconds();
31 | $minutes = (int)($seconds / 60);
32 | $seconds = fmod($seconds, 60);
33 | $hours = (int)($minutes / 60);
34 | $minutes = $minutes % 60;
35 |
36 | $result = '';
37 | if ($hours !== 0) {
38 | $result .= $hours . 'h';
39 | if ($minutes < 10) {
40 | $result .= '0';
41 | }
42 | }
43 | if ($hours !== 0 || $minutes !== 0) {
44 | $result .= $minutes . 'm';
45 | if ($seconds < 10) {
46 | $result .= '0';
47 | }
48 | }
49 | $result .= sprintf("%.{$this->scale}fs", $seconds);
50 |
51 | return $result;
52 | }
53 |
54 | private function now(): float
55 | {
56 | return microtime(true);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/StringTemplate.php:
--------------------------------------------------------------------------------
1 | template = $template;
15 | }
16 |
17 | /**
18 | * @param array $variables
19 | *
20 | * @resolve string
21 | */
22 | public function render(array $variables): \Generator
23 | {
24 | $curlyVariables = [];
25 | foreach ($variables as $name => $value) {
26 | $curlyVariables['{{' . $name . '}}'] = $value;
27 | }
28 |
29 | return strtr($this->template, $curlyVariables);
30 | yield;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/StringUtils.php:
--------------------------------------------------------------------------------
1 | $maxLength) {
20 | return substr($str, 0, $maxLength - 3) . '...';
21 | }
22 |
23 | return $str;
24 | }
25 |
26 | public static function getShortName(string $fullName): string
27 | {
28 | $parts = explode('\\', $fullName);
29 |
30 | return $parts[count($parts) - 1];
31 | }
32 |
33 | public static function getNamespace(string $fullName): string
34 | {
35 | $parts = explode('\\', $fullName);
36 | array_pop($parts);
37 |
38 | return implode('\\', $parts);
39 | }
40 |
41 | /**
42 | * @param array $matches
43 | */
44 | public static function match(string $regex, string $str, array &$matches = null): bool
45 | {
46 | $result = preg_match($regex, $str, $matches);
47 |
48 | if ($result === false) {
49 | throw new \InvalidArgumentException(__METHOD__ . '(): invalid argument');
50 | }
51 |
52 | return (bool)$result;
53 | }
54 |
55 | /**
56 | * @param string|\Closure $replacement
57 | */
58 | public static function replace(string $regex, $replacement, string $str): string
59 | {
60 | if ($replacement instanceof \Closure) {
61 | $result = preg_replace_callback($regex, $replacement, $str);
62 | } else {
63 | $result = preg_replace($regex, $replacement, $str);
64 | }
65 |
66 | if ($result === null) {
67 | throw new \InvalidArgumentException(__METHOD__ . '(): invalid argument');
68 | }
69 |
70 | return $result;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/SyncAsync.php:
--------------------------------------------------------------------------------
1 | resumeCallback !== null) {
20 | ($this->resumeCallback)();
21 | }
22 | }
23 |
24 | public function pause(): void
25 | {
26 | if ($this->pauseCallback !== null) {
27 | ($this->pauseCallback)();
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/Template.php:
--------------------------------------------------------------------------------
1 | $variables
9 | *
10 | * @resolve string
11 | */
12 | public function render(array $variables): \Generator;
13 | }
14 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Server/Utils/Throttler.php:
--------------------------------------------------------------------------------
1 | maxConcurrentJobs = $maxConcurrentJobs;
28 | $this->queue = new \SplQueue();
29 | }
30 |
31 | /**
32 | * @param mixed $job Coroutine to run.
33 | */
34 | public function run($job): \Generator
35 | {
36 | while ($this->inProgressJobs >= $this->maxConcurrentJobs) {
37 | $strand = yield Recoil::strand();
38 | $this->queue->enqueue($strand);
39 | yield Recoil::suspend();
40 | }
41 |
42 | $result = null;
43 | $this->inProgressJobs++;
44 |
45 | try {
46 | $result = yield $job;
47 | } finally {
48 | $this->inProgressJobs--;
49 | if (!$this->queue->isEmpty()) {
50 | /** @var Strand $next */
51 | $next = $this->queue->dequeue();
52 | yield Recoil::execute(function () use ($next): \Generator {
53 | $next->send();
54 |
55 | return;
56 | yield;
57 | });
58 | }
59 | }
60 |
61 | return $result;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/Symfony/Container/ServiceMapUpdater.php:
--------------------------------------------------------------------------------
1 | serviceMap = $serviceMap;
29 | $this->serviceMapWatcher = $serviceMapWatcher;
30 | $this->reflectionProperty = (new \ReflectionClass(ServiceMap::class))->getProperty('services');
31 | $this->reflectionProperty->setAccessible(true);
32 | }
33 |
34 | public function setProject(?Project $project): void
35 | {
36 | $newServiceMap = null;
37 | if ($project !== null) {
38 | $newServiceMap = $this->serviceMapWatcher->getServiceMap($project);
39 | }
40 |
41 | $this->setServiceMap($newServiceMap);
42 | }
43 |
44 | private function setServiceMap(?ServiceMap $newServiceMap): void
45 | {
46 | $services = [];
47 | if ($newServiceMap !== null) {
48 | $services = $this->reflectionProperty->getValue($newServiceMap);
49 | }
50 |
51 | $this->reflectionProperty->setValue($this->serviceMap, $services);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Tsufeki/Tenkawa/WebMozartAssert/WebMozartAssertPlugin.php:
--------------------------------------------------------------------------------
1 | setClass(AssertTypeSpecifyingExtension::class, StaticMethodTypeSpecifyingExtension::class, true);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/Tsufeki/Tenkawa/DummyTransportPair.php:
--------------------------------------------------------------------------------
1 | observer = $observer;
23 | }
24 |
25 | public function send(string $message): \Generator
26 | {
27 | yield $this->other->observer->receive($message);
28 | }
29 |
30 | public function run(): \Generator
31 | {
32 | return;
33 | yield;
34 | }
35 |
36 | /**
37 | * @return RunnableTransport[]
38 | */
39 | public static function create(): array
40 | {
41 | $ends = [new static(), new static()];
42 |
43 | $ends[0]->other = $ends[1];
44 | $ends[1]->other = $ends[0];
45 |
46 | return $ends;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Tsufeki/Tenkawa/Server/Event/EventDispatcherTest.php:
--------------------------------------------------------------------------------
1 | data = $data;
24 |
25 | return;
26 | yield;
27 | }
28 | };
29 |
30 | $container = $this->createMock(Container::class);
31 | $container
32 | ->expects($this->once())
33 | ->method('getOrDefault')
34 | ->with($this->identicalTo('OnEvent'), $this->identicalTo([]))
35 | ->willReturn([$listener]);
36 |
37 | $data = new \stdClass();
38 | $dispatcher = new EventDispatcher($container);
39 | yield $dispatcher->dispatch('OnEvent', $data);
40 | yield;
41 | yield;
42 | yield;
43 |
44 | $this->assertSame($data, $listener->data);
45 | });
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Tsufeki/Tenkawa/Server/Index/Storage/MemoryStorageTest.php:
--------------------------------------------------------------------------------
1 | emit('evt', [42]);
25 | })(),
26 | ];
27 |
28 | $this->assertSame([42], $actual);
29 | });
30 | }
31 |
32 | public function test_first_error()
33 | {
34 | $this->expectException(\RuntimeException::class);
35 |
36 | ReactKernel::start(function () {
37 | $emitter = new EventEmitter();
38 |
39 | yield [
40 | Event::first($emitter, [], ['err']),
41 | (function () use ($emitter) {
42 | yield;
43 | $emitter->emit('err', [new \RuntimeException()]);
44 | })(),
45 | ];
46 | });
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Tsufeki/Tenkawa/Server/Utils/StringUtilsTest.php:
--------------------------------------------------------------------------------
1 | assertSame($result, StringUtils::startsWith($haystack, $needle));
19 | }
20 |
21 | public function data_starts_with(): array
22 | {
23 | return [
24 | ['', '', true],
25 | ['abc', '', true],
26 | ['', 'abc', false],
27 | ['abc', 'a', true],
28 | ['a', 'abc', false],
29 | ['abc', 'ab', true],
30 | ['ab', 'abc', false],
31 | ['abc', 'abc', true],
32 | ['abc', 'abcd', false],
33 | ['abc', 'Abc', false],
34 | ['abc', 'abD', false],
35 | ['abc', 'aB', false],
36 | ];
37 | }
38 |
39 | /**
40 | * @dataProvider data_ends_with
41 | */
42 | public function test_ends_with($haystack, $needle, $result)
43 | {
44 | $this->assertSame($result, StringUtils::endsWith($haystack, $needle));
45 | }
46 |
47 | public function data_ends_with(): array
48 | {
49 | return [
50 | ['', '', true],
51 | ['abc', '', true],
52 | ['', 'abc', false],
53 | ['abc', 'c', true],
54 | ['c', 'abc', false],
55 | ['abc', 'bc', true],
56 | ['bc', 'abc', false],
57 | ['a', 'abc', false],
58 | ['abc', 'abc', true],
59 | ['abc', 'zabc', false],
60 | ['abc', 'Abc', false],
61 | ['abc', 'abD', false],
62 | ['abc', 'Bc', false],
63 | ];
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Tsufeki/Tenkawa/Server/Utils/SyncAsyncTest.php:
--------------------------------------------------------------------------------
1 | execute(function () use ($sa) {
21 | $result = $sa->callSync(function () use ($sa) {
22 | return 'foo' . $sa->callAsync((function () {
23 | yield;
24 |
25 | return 'bar';
26 | })());
27 | });
28 | yield;
29 |
30 | $this->assertSame('foobar', $result);
31 | });
32 | $kernel->run();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/Tsufeki/Tenkawa/TestCase.php:
--------------------------------------------------------------------------------
1 | kernel = ScheduledReactKernel::create();
20 | }
21 |
22 | protected function async($coroutine)
23 | {
24 | $result = null;
25 | $exception = null;
26 |
27 | $this->kernel->execute(function () use ($coroutine, &$result, &$exception) {
28 | try {
29 | $result = yield $coroutine;
30 | } catch (\Throwable $e) {
31 | $exception = $e;
32 | }
33 | });
34 |
35 | $this->kernel->run();
36 |
37 | if ($exception !== null) {
38 | throw $exception;
39 | }
40 |
41 | return $result;
42 | }
43 |
44 | public function assertJsonEquivalent($expected, $actual)
45 | {
46 | $this->assertJsonStringEqualsJsonString(json_encode($expected) ?: '', json_encode($actual) ?: '');
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Tsufeki/Tenkawa/fixtures/Foo/SelfCompletion.php:
--------------------------------------------------------------------------------
1 |