AA: introduce MPP tests for PsiTypeProvider
This commit is contained in:
committed by
Ilya Kirillov
parent
dc7506c40a
commit
d98081dce2
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.analysis.api.fe10.test.cases.generated.cases.components.psiTypeProvider;
|
||||
|
||||
import com.intellij.testFramework.TestDataPath;
|
||||
import org.jetbrains.kotlin.test.util.KtTestUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.kotlin.analysis.api.fe10.test.configurator.AnalysisApiFe10TestConfiguratorFactory;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfiguratorFactoryData;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.FrontendKind;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisSessionMode;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiMode;
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.psiTypeProvider.AbstractAnalysisApiPsiTypeProviderMultiModuleTest;
|
||||
import org.jetbrains.kotlin.test.TestMetadata;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.analysis.api.GenerateAnalysisApiTestsKt}. DO NOT MODIFY MANUALLY */
|
||||
@SuppressWarnings("all")
|
||||
@TestMetadata("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
public class Fe10IdeNormalAnalysisSourceModuleAnalysisApiPsiTypeProviderMultiModuleTestGenerated extends AbstractAnalysisApiPsiTypeProviderMultiModuleTest {
|
||||
@NotNull
|
||||
@Override
|
||||
public AnalysisApiTestConfigurator getConfigurator() {
|
||||
return AnalysisApiFe10TestConfiguratorFactory.INSTANCE.createConfigurator(
|
||||
new AnalysisApiTestConfiguratorFactoryData(
|
||||
FrontendKind.Fe10,
|
||||
TestModuleKind.Source,
|
||||
AnalysisSessionMode.Normal,
|
||||
AnalysisApiMode.Ide
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("actual_jvmInline_typealias.kt")
|
||||
public void testActual_jvmInline_typealias() throws Exception {
|
||||
runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule/actual_jvmInline_typealias.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("actual_typealias.kt")
|
||||
public void testActual_typealias() throws Exception {
|
||||
runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule/actual_typealias.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllFilesPresentInMultiModule() throws Exception {
|
||||
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule"), Pattern.compile("^(.+)\\.kt$"), null, true);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.analysis.api.fir.test.cases.generated.cases.components.psiTypeProvider;
|
||||
|
||||
import com.intellij.testFramework.TestDataPath;
|
||||
import org.jetbrains.kotlin.test.util.KtTestUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.kotlin.analysis.api.fir.test.configurators.AnalysisApiFirTestConfiguratorFactory;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfiguratorFactoryData;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.FrontendKind;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisSessionMode;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiMode;
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.psiTypeProvider.AbstractAnalysisApiPsiTypeProviderMultiModuleTest;
|
||||
import org.jetbrains.kotlin.test.TestMetadata;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.analysis.api.GenerateAnalysisApiTestsKt}. DO NOT MODIFY MANUALLY */
|
||||
@SuppressWarnings("all")
|
||||
@TestMetadata("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
public class FirIdeDependentAnalysisSourceModuleAnalysisApiPsiTypeProviderMultiModuleTestGenerated extends AbstractAnalysisApiPsiTypeProviderMultiModuleTest {
|
||||
@NotNull
|
||||
@Override
|
||||
public AnalysisApiTestConfigurator getConfigurator() {
|
||||
return AnalysisApiFirTestConfiguratorFactory.INSTANCE.createConfigurator(
|
||||
new AnalysisApiTestConfiguratorFactoryData(
|
||||
FrontendKind.Fir,
|
||||
TestModuleKind.Source,
|
||||
AnalysisSessionMode.Dependent,
|
||||
AnalysisApiMode.Ide
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("actual_jvmInline_typealias.kt")
|
||||
public void testActual_jvmInline_typealias() throws Exception {
|
||||
runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule/actual_jvmInline_typealias.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("actual_typealias.kt")
|
||||
public void testActual_typealias() throws Exception {
|
||||
runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule/actual_typealias.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllFilesPresentInMultiModule() throws Exception {
|
||||
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule"), Pattern.compile("^(.+)\\.kt$"), null, true);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.analysis.api.fir.test.cases.generated.cases.components.psiTypeProvider;
|
||||
|
||||
import com.intellij.testFramework.TestDataPath;
|
||||
import org.jetbrains.kotlin.test.util.KtTestUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.kotlin.analysis.api.fir.test.configurators.AnalysisApiFirTestConfiguratorFactory;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfiguratorFactoryData;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.FrontendKind;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisSessionMode;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiMode;
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.psiTypeProvider.AbstractAnalysisApiPsiTypeProviderMultiModuleTest;
|
||||
import org.jetbrains.kotlin.test.TestMetadata;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.analysis.api.GenerateAnalysisApiTestsKt}. DO NOT MODIFY MANUALLY */
|
||||
@SuppressWarnings("all")
|
||||
@TestMetadata("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
public class FirIdeNormalAnalysisSourceModuleAnalysisApiPsiTypeProviderMultiModuleTestGenerated extends AbstractAnalysisApiPsiTypeProviderMultiModuleTest {
|
||||
@NotNull
|
||||
@Override
|
||||
public AnalysisApiTestConfigurator getConfigurator() {
|
||||
return AnalysisApiFirTestConfiguratorFactory.INSTANCE.createConfigurator(
|
||||
new AnalysisApiTestConfiguratorFactoryData(
|
||||
FrontendKind.Fir,
|
||||
TestModuleKind.Source,
|
||||
AnalysisSessionMode.Normal,
|
||||
AnalysisApiMode.Ide
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("actual_jvmInline_typealias.kt")
|
||||
public void testActual_jvmInline_typealias() throws Exception {
|
||||
runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule/actual_jvmInline_typealias.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("actual_typealias.kt")
|
||||
public void testActual_typealias() throws Exception {
|
||||
runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule/actual_typealias.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllFilesPresentInMultiModule() throws Exception {
|
||||
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule"), Pattern.compile("^(.+)\\.kt$"), null, true);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.psiTypeProvider
|
||||
|
||||
import org.jetbrains.kotlin.analysis.api.analyze
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.psiTypeProvider.AnalysisApiPsiTypeProviderTestUtils.findLightDeclarationContext
|
||||
import org.jetbrains.kotlin.analysis.test.framework.base.AbstractAnalysisApiBasedTest
|
||||
import org.jetbrains.kotlin.analysis.test.framework.project.structure.ktModuleProvider
|
||||
import org.jetbrains.kotlin.analysis.test.framework.services.expressionMarkerProvider
|
||||
import org.jetbrains.kotlin.analysis.test.framework.utils.executeOnPooledThreadInReadAction
|
||||
import org.jetbrains.kotlin.psi.KtDeclaration
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.test.services.TestModuleStructure
|
||||
import org.jetbrains.kotlin.test.services.TestServices
|
||||
import org.jetbrains.kotlin.test.services.assertions
|
||||
|
||||
abstract class AbstractAnalysisApiPsiTypeProviderMultiModuleTest : AbstractAnalysisApiBasedTest() {
|
||||
override fun doTestByModuleStructure(moduleStructure: TestModuleStructure, testServices: TestServices) {
|
||||
val (declaration, ktFile) = moduleStructure.modules.flatMap { module ->
|
||||
val ktFiles = testServices.ktModuleProvider.getModuleFiles(module).filterIsInstance<KtFile>()
|
||||
testServices.expressionMarkerProvider.getElementsOfTypeAtCarets<KtDeclaration>(ktFiles)
|
||||
}.single()
|
||||
val containingClass = AnalysisApiPsiTypeProviderTestUtils.getContainingKtLightClass(declaration, ktFile)
|
||||
val psiContext = containingClass.findLightDeclarationContext(declaration)
|
||||
?: error("Can't find psi context for $declaration")
|
||||
val actual = buildString {
|
||||
executeOnPooledThreadInReadAction {
|
||||
analyze(declaration) {
|
||||
val ktType = declaration.getReturnKtType()
|
||||
appendLine("KtType: ${ktType.render()}")
|
||||
appendLine("PsiType: ${ktType.asPsiType(psiContext)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
testServices.assertions.assertEqualsToTestDataFileSibling(actual)
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.analysis.api.standalone.fir.test.cases.generated.cases.components.psiTypeProvider;
|
||||
|
||||
import com.intellij.testFramework.TestDataPath;
|
||||
import org.jetbrains.kotlin.test.util.KtTestUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.kotlin.analysis.api.standalone.fir.test.AnalysisApiFirStandaloneModeTestConfiguratorFactory;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfiguratorFactoryData;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.FrontendKind;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisSessionMode;
|
||||
import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiMode;
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.psiTypeProvider.AbstractAnalysisApiPsiTypeProviderMultiModuleTest;
|
||||
import org.jetbrains.kotlin.test.TestMetadata;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.analysis.api.GenerateAnalysisApiTestsKt}. DO NOT MODIFY MANUALLY */
|
||||
@SuppressWarnings("all")
|
||||
@TestMetadata("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
public class FirStandaloneNormalAnalysisSourceModuleAnalysisApiPsiTypeProviderMultiModuleTestGenerated extends AbstractAnalysisApiPsiTypeProviderMultiModuleTest {
|
||||
@NotNull
|
||||
@Override
|
||||
public AnalysisApiTestConfigurator getConfigurator() {
|
||||
return AnalysisApiFirStandaloneModeTestConfiguratorFactory.INSTANCE.createConfigurator(
|
||||
new AnalysisApiTestConfiguratorFactoryData(
|
||||
FrontendKind.Fir,
|
||||
TestModuleKind.Source,
|
||||
AnalysisSessionMode.Normal,
|
||||
AnalysisApiMode.Standalone
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("actual_jvmInline_typealias.kt")
|
||||
public void testActual_jvmInline_typealias() throws Exception {
|
||||
runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule/actual_jvmInline_typealias.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("actual_typealias.kt")
|
||||
public void testActual_typealias() throws Exception {
|
||||
runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule/actual_typealias.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllFilesPresentInMultiModule() throws Exception {
|
||||
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/psiTypeProvider/psiType/multiModule"), Pattern.compile("^(.+)\\.kt$"), null, true);
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
KtType: ERROR_TYPE
|
||||
PsiType: null
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// !LANGUAGE: +MultiPlatformProjects
|
||||
// MODULE: commonMain
|
||||
// FILE: PointerEvent.kt
|
||||
|
||||
expect class PointerEvent {
|
||||
val keyboardModifiers: PointerKeyboardModifiers
|
||||
}
|
||||
|
||||
expect class NativePointerKeyboardModifiers
|
||||
|
||||
@kotlin.jvm.JvmInline
|
||||
value class PointerKeyboardModifiers(internal val packedValue: NativePointerKeyboardModifiers)
|
||||
|
||||
// MODULE: androidMain(commonMain)
|
||||
// FILE: PointerEvent.android.kt
|
||||
|
||||
actual class PointerEvent {
|
||||
actual val <caret>keyboardModifiers = PointerKeyboardModifiers(42)
|
||||
}
|
||||
|
||||
internal actual typealias NativePointerKeyboardModifiers = Int
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
KtType: PointerKeyboardModifiers
|
||||
PsiType: PsiType:int
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
// !LANGUAGE: +MultiPlatformProjects
|
||||
// MODULE: commonMain
|
||||
// FILE: foo.kt
|
||||
|
||||
expect class Foo
|
||||
|
||||
// MODULE: androidMain(commonMain)
|
||||
// FILE: foo.android.kt
|
||||
|
||||
actual typealias Foo = Int
|
||||
|
||||
// FILE: main.kt
|
||||
|
||||
fun b<caret>ox(): Foo {
|
||||
return 42
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
KtType: Foo
|
||||
PsiType: PsiType:int
|
||||
+17
-3
@@ -86,13 +86,27 @@ class ExpressionMarkerProvider : TestService {
|
||||
|
||||
}
|
||||
|
||||
inline fun <reified P : KtElement> getElementOfTypeAtCaret(file: KtFile, caretName: String? = null): P {
|
||||
val offset = getCaretPosition(file, caretName)
|
||||
inline fun <reified P : KtElement> getElementOfTypeAtCaret(file: KtFile, caretTag: String? = null): P {
|
||||
val offset = getCaretPosition(file, caretTag)
|
||||
return file.findElementAt(offset)
|
||||
?.parentOfType<P>()
|
||||
?.parentOfType()
|
||||
?: error("No expression found at caret")
|
||||
}
|
||||
|
||||
@OptIn(PrivateForInline::class)
|
||||
inline fun <reified P : KtElement> getElementsOfTypeAtCarets(
|
||||
files: Collection<KtFile>,
|
||||
caretTag: String? = null
|
||||
): Collection<Pair<P, KtFile>> {
|
||||
return files.mapNotNull { file ->
|
||||
carets.getCaretOffset(file.name, caretTag)?.let { offset ->
|
||||
file.findElementAt(offset)?.parentOfType<P>()?.let { element ->
|
||||
element to file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelectedElement(file: KtFile): KtElement {
|
||||
val range = selected[file.name]
|
||||
?: error("No selected expression found in file")
|
||||
|
||||
+5
@@ -23,6 +23,7 @@ import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.express
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.expressionTypeProvider.AbstractHLExpressionTypeTest
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.importOptimizer.AbstractAnalysisApiImportOptimizerTest
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.psiTypeProvider.AbstractAnalysisApiExpressionPsiTypeProviderTest
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.psiTypeProvider.AbstractAnalysisApiPsiTypeProviderMultiModuleTest
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.psiTypeProvider.AbstractAnalysisApiPsiTypeProviderTest
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.signatureSubstitution.AbstractAnalysisApiSignatureContractsTest
|
||||
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.signatureSubstitution.AbstractAnalysisApiSignatureSubstitutionTest
|
||||
@@ -262,6 +263,10 @@ private fun AnalysisApiTestGroup.generateAnalysisApiComponentsTests() {
|
||||
model("psiType/forDeclaration")
|
||||
}
|
||||
|
||||
test(AbstractAnalysisApiPsiTypeProviderMultiModuleTest::class) {
|
||||
model("psiType/multiModule")
|
||||
}
|
||||
|
||||
test(AbstractAnalysisApiExpressionPsiTypeProviderTest::class, filter = frontendIs(FrontendKind.Fir)) {
|
||||
model("psiType/forExpression")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user